Galaxy

姚皓的技术博客-一杯咖啡,一首音乐,一台电脑,编程

0%

我们上一章部署都是通过神奇的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

阅读全文 »

本篇我们开始用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

阅读全文 »

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

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":[]}
阅读全文 »

我们有个需求要在打开合同PDF的时候,要将response的header里的Content-Disposition从

1
attachment;filename*="utf-8\' \'文件名"

改为

1
inline;filename*="utf-8\' \'文件名"

这样文件就可以直接在浏览器里预览打开,而不是直接下载。
理论上最好的方式自然是从应用端解决。但我们提供文件的内容管理服务器不提供这个配置选项。虽然是开源软件,但我也不想为了这个修改源代码。除此之外,为了避免影响其他和文件相关的功能,减少回归测试量,我们也不想把全局修改这个header值。
那么剩下的办法就只有从Nginx反向代理层找解决方案了。理想的解决方案是对xxx.domain.com域名(内容管理服务器的域名),所有URL中带PDF关键字和“?inline=1”参数的请求,修改header中Content-Disposition的值。(我们可以在前端请求的时候加?inline=1这个path variable)
我模糊记得Nginx可以带if条件,所以原本估计就是个小case。事实证明我估计错得离谱【捂脸】。。。如果要直接看结论的请跳转到最后一节。

教训1:Nginx“基本”不支持if里多个条件

我先找到了一段匹配文件后缀的正则表达式:

1
.*\.(后缀1|后缀2)$
阅读全文 »

在较为彻底地理解了Docker原理后,我曾经天真地以为再花差不多的时间就可以同样掌握k8s。直到我看到一张k8s的核心概念关系图:
Kubernetes核心概念关系
Docker只是其中粉红色的那一小块(Container)的一部分。。。

我也曾考虑过按照我的理解总结k8s优于Docker Swarm之处,以及为什么k8s能赢下容器编排大战。但很快发现这意味着我还需要先去熟悉Docker Swarm,才能较为准确地进行分析。这又何苦呢?我们已经知道了结论:Kubernetes是容器编排之战的最终战胜者。就算Docker Swarm有再多的优点,我们也不会采用。

我还考虑过类似Docker系列那样,先从原理开始整理。但k8s涉及的原理范围更广,从各种存储介质到OSI七层原理,先研究透再写的话估计还得花一个月。

所以我们跳过枯燥的原理介绍,先进行最有趣的实战环节,做一个包含接近完整功能的POC(Proof of Concept,概念验证)。在POC的过程中,我们再来逐渐熟悉k8s的架构设计和原理。

POC目标

我们的目标是把现在基于虚拟机的部署方式和基于脚本的打包方式,尝试用Docker容器+Kubernetes实现。
部署的时候还需要考虑:

  • 服务高可用
  • 节点扩展与收缩
  • 安全性
  • 与代码版本管理平台(Gitlab)和持续集成系统(Gitlab CI/Jenkins)的整合
  • 多版本应用维护
  • 日志收集
  • 监控
  • 数据备份
  • …其他等等

下面是一个我们当前应用架构的精简版:

阅读全文 »

一般情况下我不喜欢把部署手册放到blog里。绝大多数情况下官网已经足够详尽,而且blog很可能因为版本陈旧误人子弟。曾经我写过Nginx的二进制部署手册,早就被轻松愉快的yum安装扫进了废纸堆。而使用了Docker和K8s后yum安装方式也被迅速淘汰。Hadoop的部署也被Cloudera全自动化部署替代了。
但kubernetes的部署由于涉及科学上网的问题,把原本几个命令就能解决的问题搞得相当复杂。所以希望这篇也能多少对还在被GFW恶心的人有些帮助。(当然可能更简单的方式是部署在墙外,比如AWS上)

0. 部署目标和硬件准备

0.1 部署目标

由于是测试目的,就不部署高可用了。高可用的部署可以参见最后的参考资料。
物理拓扑结构是1 Master + 3 Worker(Worker数量可轻松扩展)。

0.2 硬件准备

部署的Kubernetes版本是v1.14.2(截止2019/5/29的最新版本),Docker的版本是Docker CE 18.09.6(也是截止2019/5/29的最新版本)。
服务器全是VMWare虚拟机。虚机的硬件和操作系统如下:

HOSTNAME ip ROLES 硬件配置 操作系统
docker-4 10.16.34.54 master 4核CPU/8GB内存/100GB硬盘 CentOS 7.4
docker-5 10.16.34.57 worker 4核CPU/8GB内存/100GB硬盘 CentOS 7.4
docker-6 10.16.34.58 worker 4核CPU/8GB内存/100GB硬盘 CentOS 7.4
docker-7 10.16.34.59 worker 4核CPU/8GB内存/100GB硬盘 CentOS 7.4

下面所有的命令都是在root账号下执行的。

阅读全文 »

在介绍完Docker的原理后,我们再回过头来看Docker的意义。

Docker的意义

事实上,Cgroups是2007年就被合并到Linux内核的功能。那个时候结合了Cgroups的资源管理能力+Linux Namespace的视图隔离能力的LXC(Linux Container)就已经产生了。
但LXC的视角还是操作系统和服务器,目标是打造一个相比虚拟机更轻量级的系统容器。
而Docker从理念上就截然不同,将目标中心转到了应用。
LXC vs Docker

不要小看这个理念上的差异。以应用为中心意味着两件事情的彻底改变:

构建和部署

测试环境和生产环境的构建和部署都是以应用为粒度的。而一个操作系统上可能部署着不同测试阶段的应用。不可能那么凑巧让上面所有的应用恰好同一周期完毕,然后将整个操作系统从测试搬到生产。这个不符合正常的开发测试流程。
而Docker直接将一个应用运行所需的完整环境,即整个操作系统的文件系统也打包了进去。只要这个应用的Docker镜像测试完成,就可以单独发布上生产。还可以利用现有的构建工具来辅助,例如Jenkins/Ansible等。遇到性能瓶颈需要横向扩展时,也可以针对单应用迅速部署启动。在性能压力消除后也可以快速回收。
在Docker之前,也已经有Cloud Foundry等PaaS项目开始以应用为中心。但相比做完一个镜像就可以随处运行的Docker,它们的便利性和适应性差了不少。

版本化和共享

制作一个操作系统容器是一件私有的事情。你制作的操作系统容器一般只会使用在你自己的团队,顶多扩展到全公司内部。别人看不到你是具体怎么做的系统容器,不清楚你是否在里面埋了雷。借我十个胆子也不敢用。
而Docker在容器镜像的制作上引入了“层(Layer)”的概念。这种基于“层”的实现借鉴了Git的思想,使容器的创建变得透明。每个人都可以审查应用容器在原始操作系统的容器上做了哪些修改。
类似在Github上开源代码,当每个人和每个公司都可以参与到全世界的应用容器分发过程中时,Docker的爆发也在情理之中了。
Docker hub

阅读全文 »

Cgroups-限制可用资源

如果应用优化得不够好,直接把CPU或内存吃光也完全不是新鲜事。但我们肯定不能容忍容器无限制地挤占宿主机的资源,甚至把宿主机搞down掉。
所以我们可以通过Linux的Cgroups(Control Groups,控制组)对某个容器可以使用的各种硬件资源设置配额。

cgroup

根据wiki,Cgroups的功能包括:

  • 资源限制(Resource Limitation):进程组使用的内存的上限,也包括文件系统与物理内存交换用的页缓存(Page Cache)
  • 优先级分配(Prioritization):控制分配的CPU时间片数量及硬盘IO吞吐,实际上就相当于控制了进程运行的优先级
  • 资源统计(Accounting):统计资源使用量,如CPU使用时长、内存用量等,主要用于计费
  • 进程控制(Control):执行挂起、恢复进程组的操作

Cgroups的设置一般分为三个主要步骤:

  1. 创建cgroup
  2. 设置cgroup配额(将cpu/内存等各种子系统添加到该cgroup中)
  3. 将进程添加为cgroup的任务

Docker容器在启动时候会动态创建Cgroup,并在容器终止的时候删除。

阅读全文 »

容器与容器间的文件系统必须相互隔离。如果一个容器能不受限制地访问到宿主机或另一个容器里的文件,那必定会引起严重的安全风险。所以对于docker中的进程,必须限制其能够访问的文件系统。
我们先来看一种简易版的实现方式:chroot。

chroot-监狱

chroot,即change root的缩写。它是一个 UNIX 操作系统上的系统调用,用于将一个进程及其子进程的根目录改变到文件系统中的一个新位置。
我们知道root根目录(/)是Linux的顶层目录。这里的顶层,换句话说就是没有办法访问比根目录更高一层级的目录。但对每个进程,可以通过chroot来“欺骗”,将指定的目录骗他们认定为根目录。

chroot经常和一个单词结合在一起说:jail(监狱)。可以很形象地理解为chroot就是给每个docker容器划了一个监狱房间。
Docker在每个监狱里配套放置了一套文件系统。

chroot

chroot的基本语法如下:

1
2
# 将某个进程
chroot /path/to/new/root command

chroot看起来挺不错,但也存在两个问题:

阅读全文 »

问题症状与解决方法

Oracle服务器由于无法上外网,所以做了个crontab的定时任务,每天定时和内部的一台ntp服务器同步。但没过两周,时间又不准了,差了近10秒。
首先排除了这个时间差是在凌晨同步完后的几小时内造成的。以“ntpdate crontab”作为关键字搜索,很容易找到了原因:坑爹的crontab重置了PATH环境变量,所以执行ntpdate命令的时候报“command not found”。
解决方法也很简单:通过whereis ntpdate的命令查出ntpdate的位置,改为完整路径调用,即“/usr/sbin/ntpdate”。另外作为以防万一,也将同步间隔从1天缩小到1小时。

为了避免下次栽坑,我们需要知道crontab到底设置了哪些PATH环境变量。
通过“vi /etc/crontab”打开文件,可以看到如下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed

可以看到PATH里明明是包含/usr/sbin,也就是ntpdate的路径。那么为什么依然会报“/bin/sh: ntpdate command not found”的错误?
(另外一个疑点是看起来是用/bin/bash执行的,为什么报/bin/sh)

crontab ≠ crontab

这里是比较容易混淆的地方:

  • crontab -e是用户任务调度的命令
  • /etc/crontab是系统任务调度的配置文件
阅读全文 »