持续集成-Jenkins离线安装与配置

本篇主要针对的是Jenkins服务器处在局域网中,无法连上互联网的情况下如何做自动化部署。
本文对网络的架设是:有内部的git服务器,yum私有仓库,nexus Repository OSS私有仓库和npm私有仓库。如果这些条件都没有,可能你们还是本地开发机上打包稍微快一些。

1. 安装

1.1 安装必要依赖

Jenkins的必要依赖是JDK。后续自动化部署的必要依赖是git,Maven和Node.js。
具体的安装步骤就不详述了。rpm安装(针对JDK)或私有YUM仓库安装都可以。
例如私有YUM仓库中openjdk,直接运行yum install java-1.8.0-openjdk.x86_64 即可。

Maven私有仓库配置
Maven打包的时候默认会从公网的仓库拉取依赖的第三方库。我们需要将其改为指向私有仓库。
首先可以通过如下两条命令之一获得配置文件地址:

1
2
mvn --version
mvn -e -X

假设settings.xml文件的位置在/etc/maven/路径下。
编辑该文件内容:

1
vi /etc/maven/settings.xml

我们首先需要在<profiles></profiles>之间添加私有仓库地址(包括仓库和插件仓库):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<profile>
<id>dev</id>
<repositories>
<repository>
<id>local-nexus</id>
<url>http://10.16.34.197:8081/repository/maven-central/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Internal Mirror of Central Plugins Repository</name>
<url>http://10.16.34.197:8081/repository/maven-central/</url>
</pluginRepository>
</pluginRepositories>
</profile>

然后在最后的</settings>上添加当前活跃的profile:

1
2
3
<activeProfiles>
<activeProfile>dev</activeProfile>
</activeProfiles>

npm私有仓库配置
npm配置私有仓库就简单多了,一条命令搞定:

1
npm set registry http://10.16.34.197:8081/repository/npm-central

1.2 安装Jenkins

Jenkins离线rpm安装包下载地址:https://jenkins.io/zh/download/
然后rpm安装jenkins(根据你下载的具体rpm包名更新命令):

1
rpm -ivh jenkins-2.176.2-1.1.noarch.rpm

然后启动jenkins:

1
systemctl start jenkins

确保防火墙已关闭或开放端口8080,然后就可以访问http://ip:8080/来访问jenkins首页了。
万一没有成功启动,可以通过systemctl status jenkins来确认失败的原因。

2. 配置

2.1 启动配置

在启动Jenkins后,首先需要解锁。
在服务器上执行:

1
cat /var/lib/jenkins/secrets/initialAdminPassword

然后输入到Unlock Jenkins的Administrator password框。
由于我们是离线模式安装,Jenkins会提醒“This Jenkins instance appears to be offline”。我们先点击“Skip Plugin Installations”跳过插件安装。
然后创建管理员用户,点击下一步。
如果有域名的话,在当前步骤的“Jenkins URL”中填写域名。我们这里直接点击“Save and Finish”。
Welcome to Jenkins:)

2.2 插件配置

在左侧菜单点击Manage Jenkins,然后点击Manage Plugins->Advanced。我们可以在这边上传所需插件的hpi文件。hpi文件可以从https://plugins.jenkins.io下载。
需要注意有一些包具有依赖。
我们需要的几个插件:

  • Publish Over SSH
  • build-pipeline-plugin
  • cron_column
  • git
  • nodejs

插件安装先后顺序如下:

  • structs
  • credentials
  • ssh-credentials
  • publish-over
  • publish-over-ssh
  • jquery
  • scm-api
  • workflow-step-api
  • workflow-api
  • junit
  • javadoc
  • display-url-api
  • mailer
  • apache-httpcomponents-client-4-api
  • maven-plugin
  • matrix-project
  • token-macro
  • run-condition
  • conditional-buildstep
  • parameterized-trigger
  • build-pipeline-plugin
  • cron_column
  • git-client
  • workflow-scm-step
  • git
  • config-file-provider
  • nodejs

当中根据提示可能需要重启数次Jenkins,也可以等所有插件都安装完再重启Jenkins。

2.3 配置工具

在左侧菜单点击Manage Jenkins,然后点击Global Tool Configuration,配置JDK、Maven和NodeJS(Git默认已配置)。
注意配置的时候取消“Install Automatically”。

配置JDK
执行

1
alternatives --config java

如果是OpenJDK,Command那列括号里/jre/bin/java之前的内容就是JAVA_HOME

配置git
Path to Git executable可以填gitusr/bin/git都可以。

配置Maven
执行如下命令

1
mvn --version

返回的结果里取Maven home即可。

配置node
可以通过npm config list命令查看当前的npm配置。node bin location后的就是node的位置。

至此Jenkins的安装和基本配置就完成了。

3. 参考资料

这篇整理完才发现Nexus Repository Manager真强大,除了maven之外,docker/npm/pypi/yum都可以proxy。
虽然也有单独npm代理的方案,比如:使用verdaccio搭建npm私有仓库 - Better’s study fairyland

本文永久链接 [ https://galaxyyao.github.io/2019/08/14/持续集成-Jenkins离线安装与配置/ ]

MySQL-没有必要的varchar(255)长度及存储汉字问题汇总

起因

最近在整理代码规范,按照之前oracle的习惯,定了以下的字段长度设定规范:

  • 名称字段:varchar(200)
  • 较长的名称字段/简介字段:varchar(500)
  • 特别长的描述字段: varchar(2000)
  • 超过2000中文字的字段:text
    为什么是200长度,而不是100或300,也是拍脑袋想的,类似DND里的房规。
    但在被问起为什么不设置为经常见到的varchar(255)时,一时回答不上来。趁这个机会,把字段长度这块的知识汇总梳理一下。

为什么会经常被设置为varchar(255)

MySQL 4.1版本之前,varchar的最大长度是255 byte字节(也有一说是5.0.3版本之前)。查了下这个版本发布都是2004年的事情了。惯性真恐怖,我可不相信还有多少系统是从2004年升级过来的。

varchar(50)和varchar(255)有性能上的差别么?

对于INNODB,varchar(50)varchar(255)这两者在存放方式上完全一样:1-2 byte保存长度,实际的字符串存放在另外的位置,每个字符1 byte到4 byte不定(视编码和实际存储的字符而定)。所以将一个字段从varchar(50)长度改成varchar(100)长度不会导致表的重建。但如果把长度从varchar(50)改成varchar(256)就不一样了,表示长度会需要用到2 byte或更多。

既然255长度以下对INNODB都一样,而且我们平时基本上也不太会使用到MYISAM,那么是不是为了省心,我们就可以把255长度以下的字段的类型都设置成varchar(255)了呢?
非也。
因为内存表介意。
虽然我们不会明文创建内存表,但所有的中间结果都会被数据库引擎存放在内存表。我们可以通过EXPLAIN或者SHOW STATUS可以查看MYSQL是否使用了内存表用来帮助完成某个操作。
而内存表会按照固定长度来保存。以utf-8编码为例,对于varchar(255),每一行所占用的内存就是长度的2 byte + 3 * 255 byte。对于100条数据,光一个varchar字段就占约1GB内存。如果我们该用varchar(50),就可以剩下来约80%的内存空间。
除此之外,255长度也可能会对索引造成坑。MySQL在5.6版本及之前的最大长度是767 byte。但MySQL 5.5版本后开始支持4个byte的字符集utf8mb4(沙雕表情用到的字符太多,长度不够用)。255 * 4 > 767,所以索引就放不下varchar(255)长度的字段了。虽然MySQL在5.7版本后将限制改成了3072 byte,但如果是多字段的联合索引还是有可能会超过这个限制。

所以我们的结论就是:在长度够用的情况下,越短越好。

varchar的最大长度是多少

varchar的最大长度是65535 byte。所以

  • 字符类型若为gbk,每个字符最多占2个字节,最大长度不能超过32766字符
  • 字符类型若为utf8,每个字符最多占3个字节,最大长度不能超过21845字符
  • 字符类型若为utf8mb,每个字符最多占4个字节,最大长度不能超过16383字符
    但通常导致varchar长度限制的通常是一行定义的长度,就是表里所有字段定义的长度总和。这个限制也是65535 byte。如果超出长度,会报错:
    1
    ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. You have to change some columns to TEXT or BLOBs。

这也是为什么阿里开发规范中这么要求:

1
【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

varchar(50)是能保存16个汉字,还是25个,抑或50个?

以前SQL Server的nvarchar转Oracle的varchar2时造成的固有印象,让我一直觉得varchar保存中文字时长度需要打对折或除以3。
但这个也是MySQL 5.0版本之前的事。现在varchar(n)是几,就能存几个中文字。
不过也需要注意统计字数使用CHARACTER_LENGTH而非LENGTH

1
2
3
4
-- 返回为12
SELECT LENGTH("轻松工作");
-- 返回为4
SELECT CHARACTER_LENGTH("轻松工作");

为什么还是用MySQL

为什么MySQL坑那么多,不改用PostgreSQL?
相比MySQL,我个人更偏好PostgreSQL,能从各种设计细节就感觉得到很规范。但无奈国内分布式数据库方案基本都是基于MySQL的。。。虽然我们的场景在今年年内暂时也看不到用分布式的必要性,但万一有了呢。。。
先发优势真是可怕。

参考资料

感谢这篇stackexchange的详细回答
database design - MySQL - varchar length and performance - Database Administrators Stack Exchange

关于内存表的详细介绍
MySQL · 特性分析 · 内部临时表

为什么索引长度会有767 byte或3072 byte的限制的详细解释
关于InnoDB索引长度限制的tips - 追风刀·丁奇 - ITeye博客

本文永久链接 [ https://galaxyyao.github.io/2019/07/30/MySQL-没有必要的varchar-255-长度及存储汉字问题汇总/ ]

前端-通过自定义协议URI Scheme,点击Chrome中的链接打开IE

有部分老Web系统只有在IE下才能正常打开。其中有一部分是即使polyfill也没法搞定的兼容性原因,另一部分就是因为使用到了ActiveX。后者中我接触到的就有金格控件和泛微OA的。
对于新开发的Portal系统,没有余力为了迁就IE,对每个功能还额外做兼容性测试。于是剩下的方案就是在单点登录跳转到相应的页面的时候,指定使用IE打开。
其实这个功能并不罕见。比如腾讯的网站上经常有点击图标打开QQ,而淘宝网页上也有很多点击打开阿里旺旺。从原理上,这是利用到了Windows自定义协议URI Scheme。

URI Scheme

自定义协议从本质上就是修改注册表。官方资料可以参考这篇Registering an Application to a URI Scheme (Windows) | Microsoft Docs
官方给了一个范例,注册一个alert://的协议,点击后打开自定义的alert.exe。

1
2
3
4
5
6
7
8
9
10
HKEY_CLASSES_ROOT
alert
(Default) = "URL:Alert Protocol"
URL Protocol = ""
DefaultIcon
(Default) = "alert.exe,1"
shell
open
command
(Default) = "C:\Program Files\Alert\alert.exe" "%1"

方案1

能看出这是一种比较通用的方案。能打开自定义的alert.exe,自然也能打开IE。所以只要将以下内容保存为test.reg,点击运行后就能将注册表项导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Windows Registry Editor Version 5.00  

[HKEY_CLASSES_ROOT\openIE]
@="URL:OpenIE Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\openIE\DefaultIcon]
@="iexplore.exe,1"

[HKEY_CLASSES_ROOT\openIE\shell]

[HKEY_CLASSES_ROOT\openIE\shell\open]

[HKEY_CLASSES_ROOT\openIE\shell\open\command]
@="cmd /c set m=%1 & call set m=%%m:openIE:=%% & call \"C:\\Program Files\\Internet Explorer\\iexplore.exe\" %%m%% & exit"

与微软官方范例的差别在于将协议改为了openIE://(这个不重要),以及最后的命令改为了一串很长的:

1
cmd /c set m=%1 & call set m=%%m:openIE:=%% & call \"C:\\Program Files\\Internet Explorer\\iexplore.exe\" %%m%% & exit

/c参数表示执行完命令后关闭窗口。所以运行的时候你会看到先弹出一个命令行窗口。该命令接收了openIE://后的参数,然后传递给IE浏览器。

对应的超链接HTML如下:

1
<a href="openIE:www.baidu.com">网络联通性测试</a>

方案1的问题

除了会弹出一个命令行窗口的问题之外,还有一个比较严重的问题:无法正确处理特殊字符&。
比如url是https://www.baidu.com?key1=value&key2=value,那么第二个参数就会丢失。有些文章提出用^&替代&可以转义,实测无效。而大部分的单点登录URL后都是跟着一串参数。所以该方案无效。

方案2

方案2做了两个改动。一个是将改动从HKEY_CLASSES_ROOT改到了HKEY_CURRENT_USER\Software\Classes下。我理解影响范围会小一些。
另一个改动是在cmd参数里将也用引号包起来,并用反斜杠转义。
修改后的注册表导入内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Classes\ie]
"URL Protocol"=""
@="URL:IE Protocol"

[HKEY_CURRENT_USER\Software\Classes\ie\shell]

[HKEY_CURRENT_USER\Software\Classes\ie\shell\open]

[HKEY_CURRENT_USER\Software\Classes\ie\shell\open\command]
@="cmd /c set url=\"%1\" & call set url=%%url:ie:=%% & call start iexplore -nosessionmerging -noframemerging %%url%%"

对应的HTML超链接如下:

1
<a href="ie:https://www.baidu.com?key1=value&key2=value2">网络联通性测试</a>

事实证明方案2可以正确处理URL参数。

最后提一下,标题中的Chrome只是指代我们常用的浏览器。并不表示Firefox或那一坨国产浏览器(恕不列举)就不能打开了。

参考资料

方案2的来源
windows - Registry - How to register Internet Explorer as a URI scheme and call from chrome? - Super User

方案1的来源
使用自定义协议实现Chrome打开IE_木子网

本文永久链接 [ https://galaxyyao.github.io/2019/07/11/前端-通过自定义协议URI-Scheme-点击Chrome中的链接打开IE/ ]

容器-13-Kubernetes实战-静态网站部署优化2-InitContainer

我们在上一篇已经将Dockerfile精简为了:

1
2
FROM nginx:alpine
COPY ./dist /usr/share/nginx/html

但相信你也发现了,内容中还是对web服务器有着强依赖。当我们想换成其他版本的Nginx镜像,或换成其他Web服务器,就必须修改源代码中的Dockerfile,重新制作镜像。
这种情况并非不可能。典型的场景之一:我们镜像所依赖的Nginx或tomcat版本出现了某个安全事故,而该问题可以通过将web服务器或web容器版本升级到最新版本解决。
所以我们希望能在Dockerfile中将Nginx的痕迹彻底抹除,只在Kubernetes的YAML中指定web服务器。

一个很自然的想法就是:我们使用一个默认的web服务器镜像。在使用该镜像的容器启动之前,将静态网站的文件拷贝到相应目录,就像在前一篇从ConfigMap获取配置文件一样。
这就是initContainer的作用。

1. initContainer

我们首先将镜像改为alpine,并修改COPY的路径:

1
2
FROM alpine:latest
COPY ./dist /html

重新编译为latest版本(这是为了强制每次重新拉取镜像):

1
docker build -t 10.16.34.197:5000/staticsite .

然后我们将deployment的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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apiVersion: apps/v1
kind: Deployment
metadata:
name: poc-web
labels:
app: poc-web
spec:
replicas: 2
selector:
matchLabels:
app: poc-web
template:
metadata:
labels:
app: poc-web
spec:
initContainers:
- image: 10.16.34.197:5000/staticsite:latest
name: poc-web-dist
command: ["cp", "/html", "/website"]
args: ["-r"]
volumeMounts:
- mountPath: "/website"
name: poc-web-volume
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
name: web
volumeMounts:
- name: poc-web-config
mountPath: /etc/nginx/conf.d
- name: poc-web-volume
mountPath: "/usr/share/nginx"
volumes:
- name: poc-web-config
configMap:
name: poc-web-config
- name: poc-web-volume
emptyDir: {}

解释一下几个改动点。

1.1 挂载emptyDir

首先我们除了ConfigMap之外,增加挂载了一个类型为emptyDir的卷。emptyDir是一个生命周期和Pod相同的空目录,作用是为多容器Pod内的容器提供一个公共盘来共享文件。当Pod从Node上被移除后,emptyDir也会随之被永久删除。缺省情况下,emptyDir使用主机磁盘进行存储的。也可以设置emptyDir.medium字段的值为Memory,来提高IO速度:

1
2
3
4
volumes:
...
- name: poc-web-volume
emptyDir: {}

1.2 增加initContainer和复制命令

然后我们在containers的平级增加一个initContainers。initContainer内定义的容器会比spec.containers内定义的容器先启动。启动的流程图可以参见下图:
Kubernetes Pod Init Process

在poc-web-dist这个容器启动后,会将emptyDir挂载到/website路径,并执行以下命令:

1
cp -r /html /website

即将包含所有的静态网站文件的html目录复制到emptyDir中。
PS. 我尝试过将command和args参数改为:

1
2
command: ["cp", "/html/*", "/website"]
args: ["-r"]

这会导致Pod启动报错。明明cp -r /html/* /website这个命令是可以正常执行的。。。目前还没找到原因。

1.3 将镜像改为nginx并挂载emptyDir

在执行完命令后initContainer完成使命退出。然后spec.container内的容器开始启动。
我们将自定义镜像改为普通的nginx镜像,并在镜像的/usr/share/nginx路径上挂载emptyDir。emptyDir中的html目录会替换nginx镜像的/usr/share/nginx/html目录,达成和之前相同的效果。

在添加了initContainer后,启动速度略微变慢,会经历一个为时十几秒的PodInitializing状态,然后正常启动:

1
poc-web-657d957f68-6m7xw     0/1     PodInitializing         0          12s

万事开头难。虽然我们目前只完成了一个静态网站的部署,但应该已经对Kubernetes有了基本的认识。

2. 参考资料

官方的Demo,通过wget下载网页后也是加载到/usr/share/nginx/html目录
Configure Pod Initialization - Kubernetes

本文永久链接 [ https://galaxyyao.github.io/2019/07/04/容器-13-Kubernetes实战-静态网站部署优化2-InitContainer/ ]

容器-12-Kubernetes实战-静态网站部署优化1:ConfigMap,Secret与TLS

虽然我们已经成功地将一个静态网站成功地在Kubernetes里部署起来了,但还有很多细节可以完善。我们就在这一节里逐步优化。

1. ConfigMap

问题最明显的是。重温一下我们静态网站之前使用的Dockerfile:

1
2
3
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
COPY ./dist /usr/share/nginx/html

首先是Nginx配置default.conf。
网站的源代码不应该干涉网站怎么部署。到底部署在Apache,Nginx还是Node.js,是否要在部署的时候添加自定义Header,都不该是开发者关注的事情。我们也不希望修改网站的timeout配置还需要动到源代码。从耦合性的角度来看,这个Nginx网站的配置文件不应该放到源代码中。
对于这类配置文件,Kubernetes里有专门的对象ConfigMap来保存。

从ConfigMap这个名字就可以猜得到,它存储的是配置信息,存储的格式是Map类型,即键值对。
配置信息可以是像本篇中的Nginx config配置,可以设置环境变量,可以是Java的properties和application.yml配置文件,可以是Redis和MySQL的配置文件。它很适合需要在一套Kubernetes集群上部署多个环境(例如特性分支/sit/uat)的情况。(当然我们的Java应用将使用Spring Cloud Config配置中心,所以目前不会用ConfigMap管理配置)
本篇POC的ConfigMap如下:

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
apiVersion: v1
kind: ConfigMap
metadata:
name: poc-web-config
labels:
app: poc-web
data:
default.conf: |
server {
listen 80;
server_name localhost;

charset utf-8;
#access_log /var/log/nginx/log/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

default.conf为key(键),下面的内容为value(值)。结构非常简单。
在Kubernetes中,ConfigMap是一种特殊的Volume(卷):Projected Volume。可以认为ConfigMap是Kubernetes中的数据被投射(Project)到容器中的。
关于Volume我们会在后续展开讨论,这里只是先提一下:要在容器中使用volume,需要先在spec中定义,然后mount到容器中。所以添加了ConfigMap后的Deployment定义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
26
27
28
29
apiVersion: apps/v1
kind: Deployment
metadata:
name: poc-web
labels:
app: poc-web
spec:
replicas: 2
selector:
matchLabels:
app: poc-web
template:
metadata:
labels:
app: poc-web
spec:
containers:
- name: poc-web
image: 10.16.34.197:5000/staticsite:latest
ports:
- containerPort: 80
name: web
volumeMounts:
- name: poc-web-config
mountPath: /etc/nginx/conf.d
volumes:
- name: poc-web-config
configMap:
name: poc-web-config

poc-web-config中的default.conf被挂到了Nginx镜像的/etc/nginx/conf.d目录下。
Nginx的nginx.conf中定义了会加载conf.d下所有conf后缀的Nginx配置:

1
include /etc/nginx/conf.d/*.conf;

而Projected Volume的挂载是在容器启动步骤最开始就进行的。所以当容器启动之前,会从ConfigMap中获取default.conf配置文件,放到/etc/nginx/conf.d目录中。当Nginx进程启动的时候,就会读到该站点的配置。
既然Nginx的配置已经由ConfigMap提供,我们就可以不需要在源代码中包含。于是Dockerfile就被精简为:

1
2
FROM nginx:alpine
COPY ./dist /usr/share/nginx/html

不管Pod在哪台宿主机上,都可以访问到ConfigMap,所以我们很容易就能猜到ConfigMap的数据保存在etcd上。
除了以YAML方式定义,还可以通过–from-file参数将文件创建为ConfigMap。
在一般情况下ConfigMap会先覆盖掉挂载目录然后再将ConfigMap中的内容作为文件挂载进行。如果想要不覆盖原本文件夹下的文件可以使用subPath参数。

1.1 热更新

更新ConfigMap不会触发Pod的滚动更新,所以每次需要修改Pod Annotation的方式来强制触发滚动更新。具体命令如:

1
kubectl patch deployment <Deployment名> --patch '{"spec": {"template": {"metadata": {"annotations": {"version/config": "20180411" }}}}}'

更多可以参考:
ConfigMap的热更新

从这个角度来说,ConfigMap不太适合保存频繁更新的配置。

2. Secret

除了ConfigMap之外,还有一种Projected Volume:Secret。从名字就可以猜得到,保存的是敏感信息,包括密码,认证token,密钥key等。
像这些比较敏感的信息,直接写在Kubernetes的Deployment YAML定义里肯定不合适。放在Secret中会比较安全和灵活。除了保存信息是加密的之外,Secret和ConfigMap并没有太大差别。
Kubernetes官网有一个生成用户名和密码的简单范例:使用 Secret 安全地分发凭证 - Kubernetes,这里就不多复述了。
这里介绍Secret,主要是因为我们接下来要给网站添加强制HTTPS访问。
要开启HTTPS访问,就先需要一个SSL证书。如果我们没有SSL证书的话,可以自己签发一个:

1
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=traefik-ui.demosite.net"

然后我们就可以通过如下的命令,根据SSL证书生成Secret:

1
kubectl create secret tls traefik-cert --key=tls.key --cert=tls.crt -n kube-system

由于是供Traefik用的,所以创建在kube-system的namespace里。接下来我们修改Traefik配置。

3. 配置TLS

3.1 配置traefik.toml

我们接下来为Traefik Ingress Controller配置证书。
Traefik的配置文件是traefik.toml。我们按照第1节的方式,将其配置为ConfigMap:

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: ConfigMap
metadata:
name: traefik-conf
namespace: kube-system
data:
traefik.toml: |
# 设置insecureSkipVerify = true,可以配置backend为443(比如dashboard)的ingress规则
insecureSkipVerify = true
defaultEntryPoints = ["http", "https"]
[entryPoints]
[entryPoints.http]
address = ":80"
### 配置http 强制跳转 https
[entryPoints.http.redirect]
entryPoint = "https"
### 配置只信任trustedIPs传递过来X-Forwarded-*,默认全部信任;为了防止客户端地址伪造,需开启这个
#[entryPoints.http.forwardedHeaders]
# trustedIPs = ["10.1.0.0/16", "172.20.0.0/16", "192.168.1.3"]
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
CertFile = "/ssl/tls.crt"
KeyFile = "/ssl/tls.key"

其中的[entryPoints.http.redirect]就是强制重定向的配置。
更多可选配置可以参考官方文档

3.2 修改Traefik Ingress Controller配置

在Ingress Controller引入ConfigMap中的配置和Secret中的证书,增加443端口。先上个配置全文:

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
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: kube-system
---
kind: DaemonSet
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
namespace: kube-system
labels:
k8s-app: traefik-ingress-lb
spec:
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
volumes:
- name: ssl
secret:
secretName: traefik-cert
- name: config
configMap:
name: traefik-conf
containers:
- image: traefik
name: traefik-ingress-lb
volumeMounts:
- mountPath: "/ssl"
name: "ssl"
- mountPath: "/config"
name: "config"
ports:
- name: http
containerPort: 80
hostPort: 80
- name: admin
containerPort: 8080
hostPort: 8080
- name: https
containerPort: 443
hostPort: 443
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
args:
- --api
- --kubernetes
- --logLevel=INFO
- --configfile=/config/traefik.toml
nodeSelector:
node: edge
---
kind: Service
apiVersion: v1
metadata:
name: traefik-ingress-service
namespace: kube-system
spec:
selector:
k8s-app: traefik-ingress-lb
ports:
- protocol: TCP
port: 80
name: http
- protocol: TCP
port: 443
name: https
- protocol: TCP
port: 8080
name: admin

首先把ConfigMap和Secret以Projected Volume的形式挂上:

1
2
3
4
5
6
7
volumes:
- name: ssl
secret:
secretName: traefik-cert
- name: config
configMap:
name: traefik-conf

然后mount到container上:

1
2
3
4
5
volumeMounts:
- mountPath: "/ssl"
name: "ssl"
- mountPath: "/config"
name: "config"

在container和service里增加443端口:

1
2
3
4
5
volumeMounts:
- mountPath: "/ssl"
name: "ssl"
- mountPath: "/config"
name: "config"

启动参数里增加config文件:

1
2
3
4
5
args:
- --api
- --kubernetes
- --logLevel=INFO
- --configfile=/config/traefik.toml

因为不太确定怎么热加载Ingress Controller,所以我采用了先delete然后重新apply。然后我们就能以https形式访问了。
顺带着Traefik Admin UI也变成https了:
Traefik Admin UI(https)

4. 参考资料

其他加载ConfigMap的方式,以及加载为环境变量的Demo
K8S学习笔记之Kubernetes 配置管理 ConfigMap - 时光飞逝,逝者如斯 - 博客园

ConfigMap加载为命令行参数和非覆盖加载的Demo
Kubernetes对象之ConfigMap - 简书

官方的ConfigMap Demo
使用ConfigMap来配置Redis - Kubernetes

网上有些配置较为过时,配置后虽然没有报错,但访问https地址就是遇到Connection Refused。下面这篇比较新一些,是本篇的主要参考:
kubeasz/ingress-tls.md at master · easzlab/kubeasz

本文永久链接 [ https://galaxyyao.github.io/2019/07/03/容器-12-Kubernetes实战-静态网站部署优化1-ConfigMap-Secret与TLS/ ]

容器-11-Kubernetes实战-Ingress与Traefik

在上一篇中,通过NodePort模式,其实我们已经可以将一系列Pod暴露给集群外。但Service最多只能做好OSI 4层的负载均衡。而OSI 7层的负载均衡需要交给Ingress。
简单解释一下,2层的负载均衡就是虚拟MAC地址接收请求;3层的负载均衡就是虚拟IP地址;4层就是基于IP + 端口;7层就涉及URI等应用层。
以下文会提到的Traefik官方的一张图来说明,Ingress的作用就是根据不同的域名,正确找到对应的后台的Service:
Traefik作用
在Service接到请求后,再负责转交给Pod:
Kubernetes Service与Deployment

要使用Ingress需要先安装一个Ingress Controller。一般比较常用的有两个:Nginx Ingress Controller和Traefik Ingress Controller。在我做POC的过程中一开始选择Nginx Ingress Controller,但总是curl调不通,于是最终选择了Traefik。Traefik还多带了一个UI不错的后台管理admin dashboard。而Nginx的不少功能需要用Nginx Plus版的才有。虽然也很理解,毕竟Nginx也是要恰饭的。。。
需要说明一点:虽然Nginx本身可以同时担任静态网站web server和反向代理两种职责,但Nginx Ingress Controller只负责反向代理。
Ingress Controller除了这两个之外,还是有F5的、Kong的和Voyager等等。从这里就可以看到Kubernetes的一大特点:指定了方向,让各厂家和开源开发者发挥自己的特长来做实现。

1. Traefik Ingress安装

安装按照官方文档一步步做就行了。
先配置RBAC(关于RBAC我们之后详细介绍):

1
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/v1.7/examples/k8s/traefik-rbac.yaml

然后配置Ingress Controller和对应的Service,对外暴露http的80端口和admin的8080端口。
Ingress Controller有两种部署方式:Deployment和DaemonSet。DaemonSet是一种特殊的Pod:

  • 在每个节点上有且仅有一个Pod实例
  • 当有新Worker节点加入时,自动在新节点上创建;旧节点被删除后,上面的DaemonSet Pod会被自动回收

两种方式各有各的好处。官方文档上都有详细比较,并在最后很贴心地对选择困难症给了建议:遇事不决先用DaemonSet试试。那么作为POC我就却之不恭选择DaemonSet了。反正要吃后悔药也就是几个命令的事情。

最后部署一个Traefik Dashboard的UI。配置的时候需要注意根据自己的情况调整host域名,然后我们就能通过域名访问admin Dashboard了。

2. Ingress YAML定义

对于Ingress来说,最关键的就是定义找服务的规则:IngressRule。
下面是最简单的Ingress模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
piVersion: extensions/v1beta1
kind: Ingress
metadata:
name: <ingress-name>
spec:
rules:
- host: <host-name>
http:
paths:
- path: /
backend:
serviceName: <service-name>
servicePort: http

如果配置过Nginx的话就很容易理解这个配置文件了。
需要注意的是:Nginx的server_name可以是ip,但Ingress的spec.rules.host必须是一个域名格式(FQDN),不能是ip。
我们的POC项目按照这个模板配置一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: poc-ingress
spec:
rules:
- host: poc.demosite.net
http:
paths:
- path: /
backend:
serviceName: poc-web-service
servicePort: http

然后配置一个DNS指向任何一个Worker节点的ip,或修改本地的hosts,就可以访问我们的静态网站了。
如果是本机测试没有域名,可以将YAML简化为:

1
2
3
4
5
6
7
8
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: poc-ingress
spec:
backend:
serviceName: poc-web-service
servicePort: 80

3. Ingress Controller高可用

我们在上文使用的DNS指定节点的方式会有不少弊端。虽然我们可以给每个域名都配置所有Worker节点的ip,即Round-robin DNS方式。但Round-robin本身就不是一个故障转移方案。在Kubernetes官方的Service章节就有提到其他缺陷。

一般有两种方式:

3.1 Deployment方式部署Ingress Controller,Service类型指定为LoadBalancer

如果使用公有云,或私有服务器有自己的LoadBalancer,一般就使用该方案了。云会给每个LoadBalancer类型的Service分配公网ip地址。但公有云的LoadBalancer服务是要收费的,而自己很难部署。

3.2 DaemonSet方式在边缘节点部署Ingress Controller,外部通过虚拟ip和keepalived访问边缘节点

首先我们可以通过label命令,选定几台服务器作为边缘节点:

1
2
kubectl label node docker-5 node=edge
kubectl label node docker-6 node=edge

然后修改traefik-ds.yaml,将通过nodeSelector,限定Ingress Controller部署在边缘节点上:

1
2
3
4
5
6
7
8
9
10
11
spec:
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
# 中间省略
nodeSelector:
node: edge

在重新apply后,可以看到traefik-ingress-controller的Pod数量降低到了2个。
然后参考这篇部署keepalived即可:边缘节点配置

Kubernetes Edge Node Architecture

4. 参考资料

Kubernetes Ingress Controller的使用介绍及高可用落地 · Service Mesh|服务网格中文社区
http://www.servicemesher.com/blog/kubernetes-ingress-controller-deployment-and-ha/

Traefik Ingress Controller的安装和使用官方文档
Kubernetes - Traefik

Nginx Ingress Controller的一个范例,虽然最后没跑起来
Kubernetes Ingress with Nginx Example - Kubernetes Book
Nginx Ingress Controller的排查手册
Troubleshooting - NGINX Ingress Controller

本文永久链接 [ https://galaxyyao.github.io/2019/06/27/容器-11-Kubernetes实战-Ingress与Traefik/ ]

容器-10-Kubernetes实战-Service

Deployment只是保证了支撑服务的Pod的数量,但是没有解决如何访问这些服务的问题。一个Pod只是一个运行服务的实例,随时可能在一个节点上停止,在另一个节点以一个新的IP启动一个新的Pod。因此不能以确定的IP和端口号提供服务。
要稳定地提供服务,需要服务发现和负载均衡能力。服务发现是一个微服务中很基础的概念,即当服务提供者网络发生变化时,服务消费者能及时获得最新的位置信息。对于k8s来说,服务提供者就是Pod,提供服务发现能力的是Service。Deployment和Service分别负责Pod的部署和访问策略,互不相关。
所以从下面这张图也可以看出来,Service和Deployment并不是上下层级的关系。

Kubernetes Service与Deployment

1. Service YAML

一个简单的Service YAML模板如下:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: <endpoint-name>
spec:
ports:
- port: <port>
name: <port-name>
targetPort: <target-port-number>
selector:
app: <app-name>

可以看到它是由selector.app来选择需要暴露的Pod。spec.ports.port是service对外暴露的端口,而targetPort是Pod的端口。默认使用TCP协议。
对于我们的POC的网站,service YAML定义如下:

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: poc-web-service
spec:
ports:
- port: 80
name: web
targetPort: 80
selector:
app: poc-web

当我们查看已部署的service的时候,可以看到该service对应的ip:

1
2
3
4
5
[root@docker-4 poc]# kubectl get service -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
hostnames ClusterIP 10.99.149.202 <none> 80/TCP 14d app=hostnames
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 28d <none>
poc-web-service ClusterIP 10.110.52.238 <none> 80/TCP 4m43s app=poc-web

甚至YAML中selector也不是必须的。可以额外定义一个映射到外部ip/域名+端口的EndPoint,然后将Service指向这个EndPoint。这个特性我理解是类似于对集群内的反向代理。如果有一个服务在测试环境调用集群内服务,生产环境调用集群外服务,只需要在service里定义两套namespace或env的label就可以解决了。

2. kube-proxy

每个Pod有自己独一无二的ip。但当一组Pod组成了一个Service对外提供服务时,只能保持一个ip对外。当我们请求这个ip时,Kubernetes需要将我们的请求相对平均地分发给每个Pod。
这个听上去像什么?没错,就是虚ip(virtual ip) + 反向代理(reverse proxy)。Kubernetes中为service提供虚ip + 反向代理的就是kube-proxy。
kube-proxy有三种实现方式:

  • userspace
  • iptables
  • ipvs

iptables是当前版本的默认。从名称上就可以猜到是通过在宿主机上设置iptables规则来实现的。由于是内核态,所以性能比用户态的userspace方式高。但节点和Pod多了之后刷新iptables规则就变成了瓶颈。所以待ipvs成熟后应该会改为ipvs。
以下这张就是的示意图:
kube-proxy

3. Service对外发布服务的方式

Service可以通过type属性,以不同的方式对外发布服务,包括:

  • ClusterIP
  • NodePort
  • LoadBalancer
  • ExternalName

3.1 ClusterIP

这是不指定type时的默认方式。暴露为一个集群内部的ip,只能在集群内部访问。这个也是我们POC采用的方式。
Kubernetes Service ClusterIP

3.2 NodePort

通过NAT的方式,在选定的数个节点上以IP形式暴露。可以通过那几个宿主机节点中的任意一个的ip+端口访问。该模式经常用于外部还有一个独立的负载均衡服务的时候使用。
Kubernetes Service NodePort

如果我们现在就急不可待想从集群外访问看一下,就可以稍微改一下配置,在ports的同一级加一个type: NodePort

1
2
3
4
[root@docker-4 poc]# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 28d
poc-web-service NodePort 10.105.188.12 <none> 80:30611/TCP 24s

如果没有指定端口的话,会随机分配一个30000-32767之间的端口。查好被分配的端口,然后就可以通过http://任意一个Worker节点的ip:30611/ 访问我们的网站了。
可以自己通过nodePort参数指定固定端口。但如果不在30000-32767的范围,就会报错:

1
The Service "poc-web-service" is invalid: spec.ports[0].nodePort: Invalid value: 80: provided port is not in the valid range. The range of valid ports is 30000-32767

为什么是任何一个Worker节点的ip都可以有效,是因为分为两种情况:

  • Pod在该宿主机节点上
  • Pod不在该宿主机节点上

如果Pod在该节点上,那么没问题IP包直接给Pod。如果不再该节点上,节点之间会做SNAT,即原地址转换。IP包会由接收到的节点转给带有Pod的宿主机节点。

1
2
3
4
5
6
7
8
9
          client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint

3.3 LoadBalancer

借用外部云服务的负载均衡能力,暴露一个固定的ip。使用公有云服务基本使用该方式。
Kubernetes Service LoadBalancer

3.4 ExternalName

通过一个固定的CNAME记录暴露,kube-dns 1.7版本之后的特性,方便实现上文提到的无selector Service。

4. 参考资料

Kubernetes Service的官方文档
Service - Kubernetes
Using a Service to Expose Your App - Kubernetes

ExternalName的一些范例
Kubernetes Tips - Part 1

各种service type的更详细介绍
Kubernetes service types

本文永久链接 [ https://galaxyyao.github.io/2019/06/26/容器-10-Kubernetes实战-Service/ ]

容器-9-Kubernetes实战-当你拍下kubectl命令背后的行为

我们上一章部署都是通过神奇的kubectl命令。我们这章就探寻一下,当我们拍下kubectl命令到Pod成功启动之间,Kubernetes究竟做了一些什么事情。
先上一张总的架构图,下面提到每个组件的时候可以在这张架构图上找位置,以及和其他组件间的关联关系:
Kubernetes component architecture

1. 全流程

1.1 Kubectl

kubectl是用于针对Kubernetes集群运行命令的命令行接口。
虽然我们是在Master节点上执行运行的kubectl,但其实kubectl也可以在本地安装,与k8s的api server远程通信交互。
kubectl在接到apply命令后,会先做一个基本的验证。如果要创建的资源不合法,或YAML格式错误,就会快速失败。
除了通过kubectl之外,也可以直接调用api,或通过dashboard UI等多种方式与api server通信。

在通信之前,kubectl需要先进行身份认证。认证信息保存在$HOME/.kube/config文件里,大致内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@docker-4 .kube]# pwd
/root/.kube
[root@docker-4 .kube]# cat config
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: <证书授权信息>
server: https://10.16.34.54:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: <客户端证书数据>

config文件中的clusters.cluster.server就是要访问的api server的地址

1.2 kube-apiserver

API Server对外暴露了Kubernetes API,用于提供查询/操作/监控服务。
当接收到来自kubectl的请求后,API Server会先做三件事:

  • 验证认证信息
  • 确认授权,即发送请求的用户有权限进行这个操作
  • 准入控制,封装了一系列额外的检查以确保操作不会产生意外或负面结果。还可以自定义插件实现自己的准入控制

1.3 etcd

etcd是一种高可用分布式存储,用于共享配置和服务发现。
之前在研究服务注册的时候还比较过它与Consul。etcd和Consul一样都是在CAP中保证CP,都是用Go语言开发的,一致性协议也都是用raft。Consul相比etcd多了多数据中心的支持。当然在k8s出现的时候还没有Consul,只有在zookeeper和etcd之间选。etcd相比zookeeper能稳定提供更大的吞吐量和延迟,而且和k8s使用的开发语言都是Go,这大概是最终选择了etcd的主要原因吧。

k8s集群将etcd当做数据库来使用,把所有的数据都存储在etcd上。当执行kubectl get命令时,结果就是从etcd中获取的。
假设kubectl执行的是创建上一篇中nginx-deployment的行为,那么最终etcd中保存的是4个对象:

  • 1个Deployment对象
  • 1个ReplicaSet对象
  • 2个Pod对象
    关于为什么还多了一个ReplicaSet对象,我们在下面说明。

1.4 Initializer初始化

在Pod还处于Pending状态,可以对Pod进行一些修改。例如给容器插入一个Sidecar容器,添加一些环境变量,挂载volume等等。Initializer初始器就是负责这个工作的。
最热门的Service Mesh–Istio项目就是通过Initializer,将Envoy容器作为Sidecar插入到每个启动的Pod中的。

1.5 控制循环

Kubernetes内部始终在运行着一个“控制循环”来实现资源的调整。
控制循环,就是控制平面的死循环。每次循环过程中,都会通过将k8s的“当前状态”和“期望状态”进行比对,来决定下一步进行什么操作。
用伪代码来描述就是:

1
2
3
4
5
6
7
8
9
for {
实际状态 := 获取集群中对象 X 的实际状态(Actual State)
期望状态 := 获取集群中对象 X 的期望状态(Desired State)
if 实际状态 == 期望状态{
什么都不做
} else {
执行编排动作,将实际状态调整为期望状态
}
}

例如当刚接收到nginx-deployment的命令时,期望是要部署2个pod,实际状态是0个pod已Ready,差额是2个:

1
2
3
[root@docker-4 deployment]# kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 0/2 2 0 3s

当部署完成后,期望状态==实际状态,部署结束:

1
2
3
[root@docker-4 deployment]# kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 2/2 2 2 19s

1.6 DeploymentController与ReplicaSet

对于每个对象类型,由kube-controller-manager对应的controller来创建。例如Deployment就对应DeploymentController。
在一些比较早的文章里,你还能看到ReplicationController,但现在它已经不再被使用。DeploymentController是其升级版,在包含了ReplicationController所有功能的基础上还增加了回滚暂停等功能。

在说明DeploymentController之前,先提一下上一章里没有提到的一个细节:Deployment和Pod之间还隔了一层ReplicaSet。
Kubernetes ReplicaSet

保持副本数量其实主要是靠ReplicaSet。从一个ReplicaSet的YAML可以看到,几乎和Deployment一模一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-set
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9

但我们之所以不直接使用ReplicaSet部署,是由于ReplicaSet的功能比较简陋。当我们想实现滚动更新的时候,就需要更上一层的Deployment支援了。

DeploymentController通过一个叫Informer的模块对Deployment、ReplicaSet和Pod的变更进行监听。
Kubernetes DeploymentController
假设上述范例中poc-deployment里的应用升级了一个版本,从v1升级到了v2。控制循环会获得一个新的期望:部署两个v2的Pod。现状是有两个v1的Pod。但此时不能立即把v1的Pod停止了,不然服务就会有一段时间不可用了。在整个滚动更新的过程中,需要保证至少有两个Pod可用,无论是v1还是v2。
所以这时候Deployment会创建一个v2的ReplicaSet,包含v2的Pod。
Kubernetes ReplicaSet V2
当v2的ReplicaSet中Pod的状态Ready后,v1的ReplicaSet就可以进行缩容为0个Pod了。

PS. 我们实际场景中可能会有会话黏连(session sticky)的情况存在。会话还处于活跃状态的Pod不应该被直接下线。怎么处理就是后话了。

1.7 kube-scheduler

Pod创建好之后,还没有被分派节点。kube-scheduler就是用来将待分派的Pod调度到指定Worker节点,并将节点与Pod的绑定信息也记录到etcd。
Master上的工作到此为止。

1.8 kubelet

每个Worker节点上会有一个Kubelet服务进程。kube-scheduler下发的任务就是由kubelet接收的。除此之外,它还负责:

  • 挂载Pod所需要的volume
  • 下载Pod的secret
  • 运行容器
  • 对容器生命周期进行检测
  • 回报节点和Pod的状态
    可以把Kubelet当成一种特殊的Controller。

至此容器正常启动,整个流程结束。

2. 声明式API

kubectl apply和docker run看上去是两句很类似的命令,但表现出来的理念截然不同。
docker run是命令式的。你发出命令,服务器接收,并按照命令创建出容器。
但Kuberentes的API是所谓的“声明式”,即你向Kubernetes提交一个定义好的API对象,声明自己想要达到的目标状态。当Kubernetes接收到这个目标状态后,自己内部协调各种组件,达成并保持这个状态。

声明式对于分布式系统有着重大的意义。

  • 首先是能实现自动化调整。分布式系统的每个组件都可能会随时发生故障。假设一个节点在部署某个Pod的过程中突然挂掉了,如果采用的是命令式API,就需要人工干预:“我换个节点再重新拍命令。等恢复那个节点后再进行之前操作的回滚”。但对于使用了声明式API的Kubernetes,会在每个控制循环的开始检查:“之前部署Pod的任务还没完成,和kubelet联系一下,问问看Node进展如何了?怎么联系不上Node?换个Node部署吧。”在挂掉的Node恢复后,它会自动调用API Server获取当前状态并进行分析:“之前要我部署的Pod已经在其他Node上部署好了?如果我继续部署的话,Pod数量就比目标多了。那么我把自己进行到一半的操作回滚吧。”整个过程完全无需外界干预。
  • 其次,对于命令式API,每个命令都是独占且阻塞的。只有先等前一个命令执行完之后才能执行下一个命令,不然就有出现冲突的可能。而声明式API使得多个写操作都能并行执行,使得处理效率大大提升。
  • 此外,声明式API还支持操作的合并。你可以设置一个YAML为基础YAML,在用户提交YAML后会和基础YAML合并,然后再提交给API Server。我感觉这有点像Java里的自定义拦截器。知名的Istio项目是主要实现原理也就是靠这种方式注入Envoy。

Kubernetes kustomize

3. 参考资料

本篇主要参考了jamiehannaford/what-happens-when-k8s: What happens when I type kubectl run?
翻译版

对k8s如何使用etcd的简要介绍
How Does Kubernetes Use etcd?

Deployment原理主要是参考这篇
详解 Kubernetes Deployment 的实现原理

关于Informer机制的更详细介绍
Kubernetes Informer 详解_Kubernetes中文社区

这篇是Kubernetes的开发者介绍的Kubernetes设计原则,值得完整读一下
Kubernetes 设计与开发原则 - 杨传胜的博客|Cloud Native|yangcs.net
原文是这篇:
Kubernetes Design and Development Explained - The New Stack

本文永久链接 [ https://galaxyyao.github.io/2019/06/25/容器-9-Kubernetes实战-当你拍下kubectl命令背后的行为/ ]

容器-8-Kubernetes实战-k8s核心概念之Node,Pod与Deployment

本篇我们开始用k8s的方式部署静态网站镜像,并通过这个过程了解k8s为什么会抽象出那么多概念。

1. Node

k8s首先需要选定在哪一台或哪几台服务器上部署。
如我们在kubeadm部署k8s集群的时候就已知的,k8s集群是由1-N台Master节点和N个Worker节点组成的。
Kubernetes节点

k8s的设计原则之一就是不挑Worker节点的硬件配置。毕竟当初Google搭Borg集群的时候淘到的服务器硬件各式各样都有。(想起来当初搭Hadoop的时候也有人问过我是不是什么硬件都可以。。。以Hadoop MapReduce的吃硬盘和网络程度,实体机+专用万兆宽带跑出来的性能比虚拟机快出好几倍。当然配置低也的确能跑起来不死。)
你手头的机器可能是什么歪瓜裂枣都有:
Kubernetes Nodes物理硬件
不管是实体机还是虚拟机,不管什么硬件配置,不管高的矮的胖的瘦的,k8s都将其一视同仁地抽象为一个Node(曾经也有一个专有名词minion)。
Kubernetes Nodes抽象

一个典型的Node信息如下(部分删减):

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
Name:               docker-7
Roles: worker
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
MemoryPressure False Fri, 21 Jun 2019 16:58:54 +0800 Fri, 21 Jun 2019 16:57:54 +0800 KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False Fri, 21 Jun 2019 16:58:54 +0800 Fri, 21 Jun 2019 16:57:54 +0800 KubeletHasNoDiskPressure kubelet has no disk pressure
PIDPressure False Fri, 21 Jun 2019 16:58:54 +0800 Fri, 21 Jun 2019 16:57:54 +0800 KubeletHasSufficientPID kubelet has sufficient PID available
Ready True Fri, 21 Jun 2019 16:58:54 +0800 Fri, 21 Jun 2019 16:57:54 +0800 KubeletReady kubelet is posting ready status
Addresses:
InternalIP: 10.16.34.59
Hostname: docker-7
Capacity:
cpu: 4
ephemeral-storage: 101729776Ki
hugepages-2Mi: 0
memory: 8010576Ki
pods: 110
Allocatable:
cpu: 4
ephemeral-storage: 93754161407
hugepages-2Mi: 0
memory: 7908176Ki
pods: 110
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 320m (8%) 300m (7%)
memory 150Mi (1%) 150Mi (1%)
ephemeral-storage 0 (0%) 0 (0%)

Master节点不断轮询更新每个Node的状态信息:“你还活着么?压力大不大?CPU/内存/存储已经分配了多少?还剩下多少?”
这样当Master节点为了新的容器需求征兵时,就能很容易地知道哪几个节点还够压榨。

2. Pod

Pod是Kubernetes中的原子调度单位。最简单的Pod就等于一个容器。这么表述也就意味着Pod里也可以放多个容器。
看为什么Kubernetes会发明出来Pod这个概念?因为有些容器之间有共享网络和存储的需求。

举个最典型的例子:日志收集器。例如我们的静态网站容器会在被访问的时候生成access.log文件。如果是在虚机里部署,那么就会在服务器上另外起一个logstash,收集这些日志文件后汇总到Elasticsearch。
如果logstash和Nginx不在同一台宿主机上部署,虽然也不是不可行,但就会很折腾:后文会提到挂载PV,可以在logstash和Nginx的Pod上都挂载同一个PV。但这个折腾毫无必要。日志文件并不那么重要,不值得永久存储,生命周期跟着容器即可。当容器被销毁时,日志也可以跟着被销毁。

那么我们是不是可以把Nginx和logstash打包到同一个容器中呢?这会产生一个问题:当logstash进程挂掉时,k8s的监控怎么表示容器的状态?如果显示Failure然后重启容器,则无辜的Nginx进程也受到了牵连;如果显示正常,那么要单独重启容器里的某一个进程就会变得很麻烦。
所以我们需要将Nginx和logstash单独打包为容器,部署成一个Pod。k8s会将一个Pod里的容器都部署在同一台宿主机上。

Kubernetes Pod

k8s的官方博客将多容器Pod的类型列为三种:

  • Sidecar(边车)容器
  • Ambassador(大使)容器
  • Adapter(适配器)容器
    k8s Pod的实现方式特别适合我们将一些控制平面的功能放到Sidecar中。近几年很知名的Service Mesh项目就是完全通过Sidecar模式支撑起来的。我们以后再详细讨论这个话题。

其实对于Pod不用想得太复杂,可以认为它就是逻辑上的一台虚机的概念。只有必须部署在同一台虚机上的,才会被并到一个Pod里。
例如虽然MySQL和Java应用虽然也可以部署在同一台虚机上,但从best practice考量,一般不会这么部署,所以就不是一个Pod。MySQL的Master和Slave一般是部署在两台服务器上,所以也是两个独立的Pod。

2.1 Pod YAML

要部署一个Pod,我们需要先写一个YAML描述文件,然后用k8s的命令部署。
我从Spring Boot的配置开始就已经接触了挺久的YAML,所以这边就不详细介绍YAML的语法了。如果有对语法部署的可以参考Wiki
一个最简单的Nginx Pod部署YAML如下:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest

这个YAML第一层的元素有四个:apiVersion, kind, metadata和spec。
apiVersion的v1表示是稳定版。(像之前kubeadm的apiVersion就是kubeadm.k8s.io/v1beta1,还在快速迭代中)。
kind就是想要创建的对象的类型。
metadata是用来识别对象的唯一性。
spec字段是对象规约,内容随着每个Kubernetes对象而不同。对于该Nginx Pod来说,需要指定使用的唯一镜像以及镜像的版本。关于镜像和镜像版本可以在Docker Hub上查到。

2.2 部署命令

我们可以使用如下的命令部署:

1
kubectl apply -f nginx-pod.yaml

然后就可以通过如下命令查看Pod创建进展:

1
kubectl get pod

以及通过如下命令查看Pod详细信息:

1
kubectl describe pod <Pod名>

想进入Pod,就是用exec -it命令:

1
kubectl exec -it nginx-pod /bin/bash

如果不需要这个Pod了,可以用以下命令取消部署:

1
kubectl delete -f nginx-pod.yaml

k8s的命令虽然很多,但都非常有规律。基本格式都是:

1
kubectl 动作 对象类型

1
kubectl 动作 [-参数] 对象类型 对象名

对于非default namespace的对象,再加一个namespace参数:

1
kubectl 动作 对象类型 对象名 -n namespace名

基本不怎么需要特别记忆。

另外提一下,在使用了k8s后,最好不要再打Docker命令了。当然自己本地开发机上跑容器的时候还是需要拍Docker命令。
在本次POC中,我们不将Nginx部署为Pod。原因在下一章中说明。

2.3 验证

此时Pod已部署成功,但我们还没法从容器外部访问。想验证的话可以稍微修改一下YAML,增加一个验证的shell容器,然后共享PID Namespace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
shareProcessNamespace: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true

上述配置在spec下增加了shareProcessNamespace: true,表示PID Namespace共享。最底下还增加一个镜像为busybox的容器。(busybox和alpine由于体积比较小,在k8s部署的时候有广泛的用途)
要部署之前,我们需要先kubectl delete -f nginx-pod.yaml把pod删除,不然会得到告警:

1
spec.containers: Forbidden: pod updates may not add or remove containers

在重新apply后使用如下的命令进入Pod

1
kubectl attach -it nginx-pod -c shell

进入busybox容器后执行ps aux,就可以看到Nginx的进程了:

1
2
3
4
5
6
7
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /pause
6 root 0:00 nginx: master process nginx -g daemon off;
11 101 0:00 nginx: worker process
12 root 0:00 sh
17 root 0:00 ps aux

2.4 infra容器:pause

上一节进程的查询结果中有一个pause进程需要说明一下。
当我们使用docker ps查看容器时,会发现有相当多的pause容器在启动。该容器和我们前几节提到的多容器Pod有关。
以我们上面修改后的nginx-pod范例为例。当我们需要让两个容器共享Namespace(不仅仅是IPC,还包括Network等其他Namespace),方法之一是让一个容器先启动,然后将另外一个容器的Namespace设置为前一个容器的Namespace。从技术上可行,但这个会导致这两个容器之间的关系不再是对等拓扑关系。
所以需要有个中间容器,也就是pause容器存在。pause容器非常小,也基本干不了什么事。它唯一的作用就是在Pod启动的最开始就启动,然后在其他容器启动后和它们共享自己的Namespace。在所有的容器启动完成后就暂停自己,不再消耗资源。
Kubernetes Infra Container

3. Deployment

Pod的副本实例数是在Deployment中定义的。
假设我们从负载和高可用的角度考虑,想要在k8s中部署2个Nginx的实例,那么只需要在Deployment的配置中定义replicas: 2即可。

我们实际写k8s yaml配置的时候,不推荐直接部署为Pod。即使是只有一个副本,也推荐部署为replicas为1的Deployment。这是由于k8s的调度机制是通过Deployment来确保副本数量。万一Pod所在的服务器挂了,k8s会检测到副本数不足1,于是将Pod调度到健康的Node上。

Kubernetes Deployment

3.1 Deployment YAML

一个Nginx Deployment的精简版的YAML如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
name: web

可以看到spec下有一个replicas,表述副本数量。

看到这个YAML,我第一反应是有些晕:为什么spec套了一层spec,template下还有一层metadata。
这篇matchLabels, labels, and selectors explained in detail, for beginners解释了为什么这个YAML有点绕的原因。关键点是这个Deployment YAML中的template其实是podTemplate。这样你就会发现template中的部分只要加上apiVersion和kind,基本就是Pod的YAML。
Kubernetes YAML template

与template平级的还有一个selector选择器属性。这个selector选择器表示deployment部署的是label里带app的podTemplate。
label作为一个非唯一的标签属性,可以使Kubernetes的运维更加灵活。这个我们后续再详细研究。

我们现在可以总结出一个最简单版的deployment yaml模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: <deployment-name>
spec:
replicas: <replicas-number>
selector:
matchLabels:
app: <app-name>
template:
metadata:
labels:
app: <app-name>
spec:
containers:
- name: <app-name>
image: <image-name>:<tag-name>
ports:
- containerPort: <container-port>
name: <container-port-name>

3.2 部署命令

部署命令和Pod基本一样:

1
kubectl apply -f nginx-deployment.yaml

然后查看到ready的deployment了:

1
2
3
[root@docker-4 pod]# kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-deployment 2/2 2 2 16h

3.3 部署POC Nginx镜像

要部署POC的Nginx镜像,只需要将image修改为私有仓库的镜像即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata:
name: poc-web
labels:
app: poc-web
spec:
replicas: 2
selector:
matchLabels:
app: poc-web
template:
metadata:
labels:
app: poc-web
spec:
containers:
- name: poc-web
image: 10.16.34.197:5000/staticsite:1.0
ports:
- containerPort: 80
name: web

如果使用Docker Hub作为Docker Registry,则首先创建secret(什么是secret会在容器-12-Kubernetes实战-静态网站部署优化1:ConfigMap,Secret与TLS | Galaxy 中详细说明):

1
kubectl create secret docker-registry regcred --docker-username=账号 --docker-password=密码 --docker-email=Docker邮箱 -n 命名空间默认default

然后在pod定义的同一级加上imagePullSecrets,例如:

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: apps/v1
kind: Deployment
metadata:
name: poc-web
labels:
app: poc-web
spec:
replicas: 2
selector:
matchLabels:
app: poc-web
template:
metadata:
labels:
app: poc-web
spec:
containers:
- name: poc-web
image: galaxyyao/demosite:1.0
ports:
- containerPort: 80
name: web
imagePullSecrets:
- name: regcred

4. 参考资料

Kubernetes Node的官方文档概念介绍
Nodes - Kubernetes

Kubernetes Pod的官方文档概念介绍
Pod Overview - Kubernetes

Kubernetes Deployment的官方文档概念介绍
Deployments - Kubernetes

这是一篇关于Pause容器的详细介绍
The Almighty Pause Container - Ian Lewis

本篇部分有趣的图来自这篇。很简要易懂的科普文。
Kubernetes & Traefik 101— When Simplicity Matters – Gérald Croës – Medium

如果想补习YAML的话可以参考这篇:
YAML basics in Kubernetes – IBM Developer

本文永久链接 [ https://galaxyyao.github.io/2019/06/24/容器-8-Kubernetes实战-k8s核心概念之Node-Pod与Deployment/ ]

容器-7-Kubernetes实战-私有仓库和打包镜像

我们先从比较简单的部分开始做。静态网站是一个比较合适的开端。独立,有界面方便验证,而且无状态。

1. 搭建Docker私有仓库

按照容器的思路,我们需要先做一个静态网站文件+Nginx的镜像,然后在服务器上把镜像拖下来后实例化为容器运行。

从安全和网络速度上考虑,我们做的这个镜像不太适合放到公网的Docker Hub上。所以要先搭个私有仓库。

我们先在一台虚机上按照kubeadm部署指南中的“2.1 安装Docker”和“2.2 启动Docker服务”两节安装docker。然后关闭防火墙。最后执行一句docker run就解决了:

1
docker run -d -p 5000:5000 --restart=always --name docker-registry registry

一开始没有关闭防火墙,在启动的时候报错了。再次启动的时候提示容器已存在。这时候只需要将docker run命令改为docker start命令就可以了。
PS. 如果是要在Windows上运行,要把端口从5000映射到5001,不然会因为和Docker Desktop端口冲突而容器启动失败。
我把Docker Registry私有仓库部署在10.16.34.197服务器上,于是现在访问http://10.16.34.197:5000/v2/_catalog,就可以看到当前还没有镜像:

1
{"repositories":[]}

2. 镜像打包服务器配置

在要打包和push镜像的服务器上(一般是Gitlab或Jenkins服务器吧),需要执行以下命令(要替换ip):

1
2
3
4
5
6
7
8
cat <<EOF > /etc/docker/daemon.json
{
"insecure-registries": [
"10.16.34.197:5000"
]
}
EOF
systemctl restart docker

要不然之后push的时候就会遇到报错信息:

1
2
3
[root@docker-4 vue-hello-world]# docker push 10.16.34.197:5000/staticsite
The push refers to repository [10.16.34.197:5000/staticsite]
Get https://10.16.34.197:5000/v2/: http: server gave HTTP response to HTTPS client

另外也需要在k8s的每个master和worker节点上都执行。不然在部署的时候也会遇到报错信息。

3. 打包网站镜像

略过不相关的web项目创建和提交代码过程,我们在Gitlab服务器上得到了需要部署的静态网站文件:

1
2
3
4
5
6
7
8
9
10
[root@docker-4 dist]# pwd
/git/vue-hello-world/dist
[root@docker-4 dist]# ll
total 12
drwxr-xr-x. 2 root root 30 Jun 13 01:42 css
-rw-r--r--. 1 root root 4286 Jun 13 01:42 favicon.ico
drwxr-xr-x. 2 root root 31 Jun 13 01:42 img
-rw-r--r--. 1 root root 730 Jun 13 01:42 index.html
drwxr-xr-x. 2 root root 126 Jun 13 01:42 js
[root@docker-4 dist]#

3.1 添加Nginx配置和Dockerfile

要将该网站host在Nginx上,只需要做两个步骤:

  • 将dist里的文件复制到Nginx的/usr/share/nginx/html目录下
  • 做一个conf配置,根目录/指向/usr/share/nginx/html,并设置默认首页网页文件名index.html

于是在网站下增加了一个default.conf文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 80;
server_name localhost;

charset utf-8;
#access_log /var/log/nginx/log/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

另外增加了一个Dockerfile:

1
2
3
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
COPY ./dist /usr/share/nginx/html

nginx:alpine是Nginx官方的轻量级镜像。

3.2 执行打包命令

然后到Dockerfile所在目录构建镜像并推送到私有仓库:

1
2
docker build -t 10.16.34.197:5000/staticsite:1.0 .
docker push 10.16.34.197:5000/staticsite

现在访问私有仓库的catalog目录,就能看到repositories中已增加了一个静态网站的镜像:

1
{"repositories":["staticsite"]}

也可以不指定镜像版本,那么打出来的版本就是latest:

1
docker build -t 10.16.34.197:5000/staticsite .

可以通过如下的URL查看该镜像的所有可用版本:
http://10.16.34.197:5000/v2/staticsite/tags/list
(要访问其他镜像替换staticsite这个镜像名就可以了)

4. 接下来的步骤

如果只用docker,那么执行如下的命令就结束了:

1
docker run -itd --name staticsite --publish 8080:80 10.16.34.197:5000/staticsite:1.0

但到此为止的话,我们还是需要明确地指定在哪台机器上部署该容器。如果需要在多台机器上部署为高可用,就需要重复N遍同样的操作。为了保持高可用,还需要在部署容器的服务器前面额外部署负载均衡。
作为懒人,我很希望只要定义好需要的副本数量以及每个容器的硬件需求,就有人能替我把这些事情都包办了。万一哪个容器挂了还能迅速自动重启。
k8s就是这样的集群容器管家。我们下一章继续。

PS. Private Registry也可以部署在k8s上(所以上一章的架构图中它也在虚线框内),但为了简化POC的流程,我们先把这个放在后面做。

5. 使用Docker Hub作为Docker Registry

自己Demo的时候也可以省掉这个步骤,直接将镜像上传到Docker Hub,然后K8S从Docker Hub上拉取镜像。
在注册Docker Hub账号之后,如果是Windows机器,需要打开Docker Desktop,然后输入docker login就可以直接登录了,不需要再输入密码。如果是Linux上,则需要在docker login命令后输入用户名和密码。
构建镜像和推送命令需要改为:

1
2
docker build -t 账号/staticsite:1.0 .
docker push 账号/staticsite

在K8S集群里拉取镜像的步骤在下一章再介绍。

更详细步骤可以参考Pull an Image from a Private Registry - Kubernetes

6. 流程图

画了一下按角色划分的发布流程图。不管用的是k8s还是docker swarm,CI/CD的流程还是基本类似的。
容器CI/CD流程

7. 参考资料

这是本篇参考的一个最精简的静态网站的Docker配置
nishanttotla/DockerStaticSite: A simple static website using Docker and Nginx

本文永久链接 [ https://galaxyyao.github.io/2019/06/18/容器-7-Kubernetes实战-私有仓库和打包镜像/ ]