之前也尝试过Github Action相关的工具进行语雀和博客的持续集成,但是它会依赖语雀Webhook、函数计算服务、Github Action等,其中一个出问题,那么整个流程就不可用。因此在“尽量减少外部依赖”的思路下,除了必须的Hexo仓库和语雀,重新在Kubernetes集群上构建了基于Elog+Hexo博客的持续集成流程。
你需要做的前期准备工作是:
整个博客自动化的流程如上图所示:
从本地SSH私钥文件创建Secrets, 需要确认这个私钥对应的公钥已经在git仓库中配置过
1 | kubectl crete ns blog |
按如下blog.yaml文件修改对应配置后,部署Hexo博客相关的资源至集群中
1 | apiVersion: v1 |
总结下上面配置中的注意点,必须修改的包括:
可能需要修改的包括:
等待第二步中的博客部署成功后,按如下elog-yuque-syncer.yaml文件部署定时同步专用的Pod。Pod启动时会默认执行一次博客重建,可以用来验证任务是否能够执行。
1 | apiVersion: v1 |
部署完毕后,执行以下命令观察博客Pod是否正常删除
1 | hexo kubectl -n blog logs -l app=elog-yuque-syncer |
工具镜像Dockerfile如下
1 | FROM --platform=linux/amd64 alpine@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 |
本系列主要基于v1.24.0版本的Kubelet部分源代码,进行Kubernetes中容器监控的底层原理介绍与代码分析。
前面讲到,在Kubelet的代码中,关于数据来源StatsProvider的实现有两种:CriStatsProvider(下称”CRI实现”)和CadvisorStatsProvider(下称”cAdvisor实现”)。从命名上大致可以猜测,前者是为了降低对cAdvisor的直接依赖而将数据的交互限定在CRI API层面,后者则是对cAdvisor进行直接调用的,接下来本文就对这个想法进行验证分析。
查看StatsProvider的构造器方法参数,对比前文列出的handler中的Provider接口定义,可以看出Provider接口方法除了kubelet本身实现的部分,监控相关的接口在Stats包中的实现被拆分为了几大部分:
1 | func newStatsProvider( |
1 | type containerStatsProvider interface { |
1 | // collector对prometheus adapter的调用 |
1 | containerMap, podSandboxMap, err := p.getPodAndContainerMaps() |
1 | func (p *criStatsProvider) listPodStatsPartiallyFromCRI(updateCPUNanoCoreUsage bool, containerMap map[string]*runtimeapi.Container, podSandboxMap map[string]*runtimeapi.PodSandbox, rootFsInfo *cadvisorapiv2.FsInfo) ([]statsapi.PodStats, error) { |
1 | func (p *criStatsProvider) ImageFsStats() (*statsapi.FsStats, error) { |
1 | func (p *criStatsProvider) ImageFsDevice() (string, error) { |
本系列主要基于v1.24.0版本的Kubelet部分源代码,进行Kubernetes中容器监控的底层原理介绍与代码分析。
本文主要从Kubelet Server提供的监控API出发,介绍其分类和作用,然后对这些接口的数据来源进行简单的分析。
在Kubelet Server提供的监控API中,大致可以分为两类:stats(统计数据)和metrics(指标数据)。从命名和实际作用来看,前者提供了粗粒度的基础监控能力,目前用于各种内置组件;而后者用于持久化地进行细粒度的容器监控,主要提供给Prometheus等。
在v1.24.0版本中,目前统计类接口仅包含/stats/summary,该接口提供了节点和Pod的统计信息。节点部分包括CPU、内存、网络、文件系统、容器运行时、Rlimit的统计。Pod部分主要提供Pod相关的基础统计与卷、临时存储、进程统计外,主要还包括了各个容器的统计信息。容器部分值得关注的是,该接口中提供了用户自定义指标。方法实现如下:
1 | func (h *handler) handleSummary(request *restful.Request, response *restful.Response) { |
查看SummaryProvider的实现可以发现,该实际上就是对stats.Provider的封装。
1 | type SummaryProvider interface { |
在handler的处理逻辑中,它提供了一个只返回CPU和Memory信息的选项onlyCPUAndMemory,如果只关心cpu和内存信息,通过此选项可以去除多余的统计信息,metrics-server(_< 0.6.0版本_)中就默认设置了该值。该部分需要注意的是,如果获取的是完整信息,那么监控信息是从缓存中获取的,这里特指CPU中的NanoCoreUsage不会更新,只有调用onlyCPUAndMemory才会将该值更新。如果基于接口做定制开发需要将forceStatsUpdate修改为true以保证NanoCoreUsage的准确性。
此外,在最新版本的0.6.x版本的metrics-server中已经不再依赖该接口,而是采用了Kubelet中的/metrics/resource接口进行资源的监控,在老版本集群中部署metrics-server时需要注意不兼容问题。
Kubelet Server提供的指标类API目前包括以下四个:
上述四个接口返回的指标信息默认都是Promtheus格式。一般来说,指标想要转化为Promtheus格式需要实现Prometheus client的Registerer和Gatherer接口,而在K8s中对应的封装实现就是KubeRegistry。这里我们先跳过Prometheus client的实现原理和其他非容器指标相关的实现,而是来看看容器指标数据是如何获取到并转化返回的。
1 | r := compbasemetrics.NewKubeRegistry() |
在Prometheus Client的注册逻辑里,返回数据需要实现指标收集器(Collector)部分。这里可以看到,Kubelet提供了两种数据收集器,一部分是Pod/容器的指标收集器,另一部分是节点指标收集器。
1 | func NewPrometheusMachineCollector(i infoProvider, includedMetrics container.MetricSet) *PrometheusMachineCollector { |
可以看到收集器实际上主要是通过getValues方法把infoProvider提供的数据结构从原始结构转成Prometheus中的指标,同时每个指标需要定义其名称、类型以及描述。节点和Pod/容器对应的原始数据结构都定义在cAdvisor的API Spec中,即MachineInfo和ContainerStats。
1 | type infoProvider interface { |
而从上面KubeRegistry的注册逻辑中可以看到,infoProvider的实现又需要通过prometheusHostAdapter进行一次转换,其转换前的实现接口为HostInterface。
1 | type prometheusHostAdapter struct { |
在HostInterface的定义中,查看监控相关的部分接口,prometheusHostAdapter需要的VersionInfo和MachineInfo是由Kubelet实现的,而GetRequestedContainersInfo即容器相关监控信息的接口则由前面提到的stats.Provider中实现。
1 | type HostInterface interface { |
基于上一节分析,可以了解到Kubelet Server中返回的监控信息,无论是统计类信息还是指标类信息,容器相关的部分都是通过stats.Provider获取的。那么我们看看stats.Provider部分是如何定义的:
1 | type Provider interface { |
通过上面Provider的接口划分,我们大致了解了Kubelet在监控方面提供的能力,主要包括容器、Pod、节点、文件系统的统计信息或指标信息等。其中不少接口都用到了cgroup。
cgroup(control group)是Linux内核中用于限制、记录和隔离一组进程的资源使用(CPU、内存、磁盘 I/O、网络等)的模块,它与namespace一起作为基石构成了容器基础技术的实现。绝大部分场景下,大家对他比较熟悉的点在于其发挥的资源划分和限制能力,例如K8S中Pod与Container的CPU/内存资源Limit。除了资源限制能力外,实际上cgroup每个子系统还具有对应的统计能力。以CPU子系统为例,它可以统计CPU在用户空间和内核空间的运行时长分配、设置Limit后的CPU限流次数和限流总时长等:
Cgroup是以树形结构来划分和组织系统内的各个进程的,Kubelet在运行时会创建一个kubepods的叶节点。而根据Pod的QoS等级划分,不同Pod被放在不同的QoS叶节点下。对应的,容器本身作为Pod管理的最小单元,其被划分在Pod的叶节点以下(_需开启containerd中runc的SystemdCgroup选项,否则container由containerd cgroup单独管理_)。
大致了解了Cgroup之后,细心的你就能想明白在Provider中不太容易理的GetRawContainerInfo和GetRequestedContainersInfo接口的用法,即参数中的Container指的不仅是被Pod管理的Container,其实指的也是Cgroup的叶节点。它可以是Container/Pod/QoS/Kubepods,甚至是节点本身(即”/“)。这也能解释为什么会有subcontainers这个参数,毕竟Container作为最小运行单元肯定是不存在子容器这个说法的。subcontainers可以直接理解为是否包含子节点的信息,GetRawContainerInfo接口实际上是提供了当前节点与子节点的统计信息。
provider初始化包含在Kubelet的初始化流程中,通过useLegacyCadvisorStats开关,Kubelet会进行会在CadvisorStatsProvider和CRIStatsProvider中选择一种作为实际实现。
1 | func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *Dependencies, ... ){ |
对比两者的初始化方法可以分析得出其主要的数据来源都是cadvisor和resourceAnalyzer,其他参数是主要作为一些辅助选项。resourceAnalyzer主要提供了节点资源消耗的统计,除了文件系统相关的统计,其主要实现还是我们之前提到的SummaryProvider。
那么基本可以得出一个结论,无论是哪种实现,容器相关的监控都主要来自于cAdvisor。
本篇文章中,我们主要了解了Kubelet Server在对外提供的监控API中统计类和指标类的划分,并了解到容器监控的部分主要由Stats Provider定义。同时,我们也知道了目前Kubelet中对于Stats Provider的实现主要有两种——CadvisorStatsProvider和CRIStatsProvider。接下来的一篇文章,我们会对这两种实现进行深入的对比分析,并搞清楚两种Provider的实现是如何使用cAdvisor接口的。
]]>Image GC是kubelet的镜像清理功能,用于在磁盘空间不足的情况下清除不需要的镜像,释放磁盘空间,保证Pod能正常启动运行。
Kubelet默认开启,通过kubele启动配置中的ImageGCPolicy控制。ImageGCPolicy有三个设置参数:
ImageGCHighThresholdPercent:触发gc的阈值,超过该值将会执行gc,设置为100时,gc不启动。
ImageGCLowThresholdPercent:ImageGC执行空间空间的目标值,gc触发后,将会将磁盘占用率降至该值以下;
ImageMinimumGCAge:最短GC年龄(即距离首次被探测到的间隔),小于该阈值时不会被gc。
在kubelet启动时,ImageGC的启动在BirthCry执行完成之后。
1 | func (kl *Kubelet) StartGarbageCollection() { |
可以看到,ImageGC由单独的协程执行,默认的执行间隔为五分钟。当ImageGC首次执行失败时会打印日志,而重复失败后,会记录一个ImageGCFailed的事件。这意味着可以通过配置日志或者告警了解GC是否正常运行。
接下来看看ImageGCManager的具体实现。
1 | type ImageGCManager interface { |
ImageGCManager的接口非常简单,只有四个方法:
GarbageCollect:根据定义的ImageGCPolicy执行具体的清理动作;
Start:异步地收集镜像信息;
GetImageList:获取缓存中的镜像列表;
DeleteUnusedImages:删除未使用的镜像。
ImageGCManager在初始化时会校验Policy的参数合法性,然后传递运行时、监控、事件等参数。然后看看Start方法的逻辑:
1 | func (im *realImageGCManager) Start() { |
ImageGCManager的Start方法会启动两个协程。在第一个协程内,每隔五分钟Manager会检查一次镜像。一旦完成一次,Manager的状态就会被标记为已初始化。另一个协程每隔30秒会从容器运行时获取所有的镜像信息,更新到缓存的镜像列表中。
那么,Manager时如何检测镜像的呢?
1 | func (im *realImageGCManager) detectImages(detectTime time.Time) (sets.String, error) { |
检测镜像的目的是找出正在使用的镜像,防止在GC执行的过程中被清理。同时,在此过程中,镜像的清理需要参考一些信息,这些信息也会在检测的过程中更新。
首先,Sandbox镜像是一定会被判定为正在使用的镜像。接着会将所有Pod的所有正在运行中的容器使用的image加到正在使用的镜像列表中。注意,即使Pod有容器需要该镜像,但是该容器未处于Running状态,其对应的镜像也会被清理。
选出正在使用(即不会被清理)的镜像之后,会将容器运行时中获取到的镜像列表信息更新到Manager维护的镜像列表记录中。
查询所有新获取的镜像列表信息进行遍历,分为以下几步:
最后,如果某个镜像已经不在容器运行时返回的镜像列表中,就会被移出Manager缓存的镜像探测记录。
ImageGCManager的核心方法就是GarbageCollect了,主要步骤如下:首先获取Image对应的Filesystem占用信息,根据启动的配置计算出用量百分比以及需要释放的空间大小,然后开始释放。如果实际释放的空间小于目标大小,会记录FreeDiskSpaceFailed的Warnning事件。
1 | func (im *realImageGCManager) GarbageCollect() error { |
算出需要释放空间后是删除的镜像是怎么决定的呢?在开始执行清理时,会执行我们上面介绍的镜像探测过程。在完成镜像探测后,我们的得到的imagesInUse包括了Sanbox镜像以及Pod内正在与运行中容器使用的镜像。接下来,要选出清理的目标镜像,存放清理目标的数据结构叫evictionInfo,它存放了所有不在imagesInUse列表内的镜像记录。接着会将这些镜像记录按照 最后使用时间 和 首次探测时间 进行一次排序,即按照LRU规则将最后一次使用时间较早和探测事件较早的镜像排在前面。
排序完之后,会遍历所有这些镜像:如果是镜像最后一次使用事件没有删除触发时间早(即刚刚刷新了最后使用时间),则不会删除。同时,如果该镜像首次被探测到的时间差小于配置的最小GC间隔(即刚加入到缓存记录中),也不会删除。否则,就会依序删除这些镜像,删除完之后会从探测记录中删除该镜像同时累加 已经释放的空间值spaceFreed。如果spaceFreed不小于目标释放的空间,则本轮的清理正常结束。
1 | func (im *realImageGCManager) freeSpace(bytesToFree int64, freeTime time.Time) (int64, error) { |
除了上述GC逻辑外,实际上还有额外的ImageGC触发条件。在运行中,偶尔会遇到ImageGCHighThresholdPercent被设置为100但还是有镜像被清理的情况。我们反过来看下在上文提到的ImageGC的接口,可以看到DeleteUnusedImages是个public方法。
1 | func buildSignalToNodeReclaimFuncs(imageGC ImageGC, containerGC ContainerGC, withImageFs bool) map[evictionapi.Signal]nodeReclaimFuncs { |
实际上,在磁盘满导致节点驱逐信号触发时会直接调用容器和镜像的GC方法,毕竟节点驱逐的触发是更紧急的。
总的来看,Kubelet会在节点驱逐信号触发和Image对应的Filesystem空间不足的情况下删除冗余的镜像。整个GC的要点如下:
一开始就把博客写作叫成一个工作流,是因为我们很多时候在写博客的时候,懒惰的很大一部分原因是因为觉得麻烦。当你在使用hexo这种静态博客的时候,难免会觉得写作是一件很繁琐的时间,首先会受限于写作环境,其次每次写作都需要“写md –> 插入图片上传 –> 预览后编译 –> 推送到pages”。因为工作的原因,用了很久的语雀,感觉语雀的写作体验和管理都是很棒的。那么,如何拥有语雀写作的体验,又能够免去博客更新的繁碎流程呢?
实现配置的文章网上一堆,我就不当个搬运工了,说下整个流程吧:
完成后,获得的是这样的一套写作平台体验:
当然了,初次配置的成本也是不低的。不过对于程序员来说,哪怕首次配置麻烦,后面能省掉很多时间成本也是一件很赚的事情吧。(何况语雀比本地markdown好用多了)而且每次写作不用限制在某一台电脑上。
title: 理解Kubernetes对象
english_title: understanding-kubernetes-object
date: 2020-11-2 23:00:0
tags:
categories:
Kubernetes集群内,对象是持久化的实体。Kubernetes使用对象来表征集群的状态,对象表述了应用的运行状况、应用可用的资源、应用的运行策略(重启、升级、容错)。_
A Kubernetes object is a “record of intent”–once you create the object, the Kubernetes system will constantly work to ensure that object exists. By creating an object, you’re effectively telling the Kubernetes system what you want your cluster’s workload to look like; this is your cluster’s desired state.
To work with Kubernetes objects–whether to create, modify, or delete them–you’ll need to use the Kubernetes API. When you use the kubectl
command-line interface, for example, the CLI makes the necessary Kubernetes API calls for you. You can also use the Kubernetes API directly in your own programs using one of the Client Libraries.
其他:
process -> process group(cgroup) -> hierarchy
树节点 -> 树
subsystem name
subsystem关联hierarchy id:如果为0,无绑定/cgroup v2绑定/未被内核开启
num of cgroups:关联hierarchy内进程组个数
enabled:是否开启,通过内核参数cgroup_disable调整
cpu (since Linux 2.6.24; CONFIG_CGROUP_SCHED)
用来限制cgroup的CPU使用率。
cpuacct (since Linux 2.6.24; CONFIG_CGROUP_CPUACCT)
统计cgroup的CPU的使用率。
cpuset (since Linux 2.6.24; CONFIG_CPUSETS)
绑定cgroup到指定CPUs和NUMA节点。
memory (since Linux 2.6.25; CONFIG_MEMCG)
统计和限制cgroup的内存的使用率,包括process memory, kernel memory, 和swap。
devices (since Linux 2.6.26; CONFIG_CGROUP_DEVICE)
限制cgroup创建(mknod)和访问设备的权限。
freezer (since Linux 2.6.28; CONFIG_CGROUP_FREEZER)
suspend和restore一个cgroup中的所有进程。
net_cls (since Linux 2.6.29; CONFIG_CGROUP_NET_CLASSID)
将一个cgroup中进程创建的所有网络包加上一个classid标记,用于tc和iptables。 只对发出去的网络包生效,对收到的网络包不起作用。
blkio (since Linux 2.6.33; CONFIG_BLK_CGROUP)
限制cgroup访问块设备的IO速度。
perf_event (since Linux 2.6.39; CONFIG_CGROUP_PERF)
对cgroup进行性能监控
net_prio (since Linux 3.3; CONFIG_CGROUP_NET_PRIO)
针对每个网络接口设置cgroup的访问优先级。
hugetlb (since Linux 3.5; CONFIG_CGROUP_HUGETLB)
限制cgroup的huge pages的使用量。
pids (since Linux 4.3; CONFIG_CGROUP_PIDS)
限制一个cgroup及其子孙cgroup中的总进程数。
1 | # 挂载一颗和cpuset subsystem关联的cgroup树到/sys/fs/cgroup/cpuset |
查看进程对应的cgroup
proc/[pid]/cgroup
hierarchy id : subsystems : 进程在hierarchy中的相对路径
cgroup.clone_children
这个文件只对cpuset subsystem 有影响,当该文件的内容为1时,新创建的cgroup将会继承父cgroup的配置,即从父cgroup里面拷贝配置文件来初始化新cgroup,可以参考这里
cgroup.procs
当前cgroup中的所有进程ID,系统不保证ID是顺序排列的,且ID有可能重复
notify_on_release
该文件的内容为1时,当cgroup退出时(不再包含任何进程和子cgroup),将调用release_agent里面配置的命令。新cgroup被创建时将默认继承父cgroup的这项配置。
release_agent
里面包含了cgroup退出(移出)时将会执行的命令,系统调用该命令时会将相应cgroup的相对路径当作参数传进去。 注意:这个文件只会存在于root cgroup下面,其他cgroup里面不会有这个文件。
tasks
当前cgroup中的所有线程ID,系统不保证ID是顺序排列的
新建子文件夹
在一颗cgroup树里面,一个进程必须要属于一个cgroup。
新创建的子进程将会自动加入父进程所在的cgroup
从一个cgroup移动一个进程到另一个cgroup时,只要有目的cgroup的写入权限就可以了,系统不会检查源cgroup里的权限。
用户只能操作属于自己的进程,不能操作其他用户的进程,root账号除外
打印当前shell pid
echo $$
1 | sh -c 'echo 1421 > ../cgroup.procs' |
将pid写入,即是进程加入cgroup。pid不可在文件中删除,只可以被转移。因为 在一颗cgroup树里面,一个进程必须要属于一个cgroup
pids.current: 表示当前cgroup及其所有子孙cgroup中现有的总的进程数量
pids.max: 当前cgroup及其所有子孙cgroup中所允许创建的总的最大进程数量
通过写入pids.max限制成功
pids.current > pids.max 出现的情况:
1 | cgroup.event_control #用于eventfd的接口 |
设置了内存限制立即生效 –> 物理内存使用量达到limit –> memory.failcnt +1 –> 内核会尽量将物理内存中的数据移到swap空间上去 –> 设置的limit过小,或者swap空间不足 –> kill掉cgroup内继续申请内存的进程(默认行为)
详细链接
cfs_period_us:时间周期长度
cfs_quota_us:在一个周期长度内所能使用的CPU时间数
cpu.shares:
limit_in_cores = cfs_period_us / cfs_quota_us
m=milli unit,表示千分之一
M=1000
cpu/mem/gpu/huge-page(v1.14)
1 | resources: |
调度时不看实际的使用资源量,看已运行pod的request总和作为已占用资源的度量
分为 完全可靠资源 和 不可靠资源,通过这种机制实现 超卖
完全可靠的资源 = request
不可靠资源 = limit - request
空闲资源按照Request的比例进行分配
pod的cpu使用超过limit时,cgroups会对pod进行限流throttled
超过request可能会被杀掉
超过limit时,内核会杀掉容器中使用内存最多的一个,直到不超过limnit为止
优先级递减:Guaranteed > Burstable > BestEffort
所有容器request=limit(仅设置Limit时也等效)
requests不等于limits
所有容器的request和limit都未定义
OOM Score = 内存占用百分比 * 10 + 调整分(OOM_SCORE_ADJ)
Guaranteed: -998, BestEffort: 1000,
Busrtable:
系统调用:内核的接口
应用程序既可以使用共用函数库,也可以使用系统调用
登录项的组成:登录名、加密口令、数字用户ID(UID)、数字组ID(GID)、注释字段、起始目录、shell程序
每个进程都有一个工作目录(当前工作目录),相对路径都从工作目录开始解析
进程可以使用chdir函数更改工作目录
文件描述符是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表
取值范围为0 到 OPEN_MAX(每个进程最多可以打开 的文件数-1,63)
标准输入、标准输出和标准错误是在运行新程序时,shell为程序默认打开的3个文件描述符
内核使用exec函数将程序读入内存并执行程序
fork函数创建一个新进程,该进程(子进程)是调用进程(父进程)的一个副本
在创建时,对父进程返回子进程ID,对子进程返回0
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性
errno:Unix系统函数出错时返回的负值
关于errno:
关于错误:
出错分为致命性的和非致命性的
对于致命性错误,可打印出错消息或者写入日志
对于非致命错误,可以尝试重试(延时、指数补偿算法)
用户不可更改其用户ID
用户ID为0的用户为root用户或超级用户
组文件将组名映射为数字的组ID,位于/etc/group
系统允许用户属于另外一些组,一个用户属于多至16个其他的组
信号用于通知进程发生了某种状况
处理信号的方式:
忽略
系统默认方式处理
提供函数,信号发生时调用,即捕捉信号
日历时间:自UTC时间的秒数累计值
进程时间:也叫做CPU时间,用来度量进程使用的CPU资源,以时钟tick计算
系统为进程维护的三个进程时间值:
时钟时间,进程运行的时间总量(与系统同时运行的进程数有关)
用户CPU时间,执行用户指令所用的时间量
系统CPU时间,为该进程执行内核程序经历的时间量
用户CPU时间和系统CPU时间常称为CPU时间
1 | # 度量执行时间 |
系统调用:提供给程序向内核请求服务的,操作系统提供的入口点
区别:
(1)不要破坏软件功能,让失误率无限接近于0
(2)不要破坏结构
职业发展需要保证每周有自己的时间
能就是能,不能就是不能。不要说“试试看”。
作为专业人士,就不应该什么事都照做
面对艰难的决定,直面不同角色的冲突是最好的办法——找到可能的最好结果
“为什么”其实并没有那么重要
在高风险时刻说不,是对大家负责
坚守专业主义精神,说不
不说“试试”,直接说出所有的可能性
坚守原则
这种状态下,感觉效率极高、绝无错误
避免进入流态区,因为这样会进入“放弃顾及全局”的陷阱
调试和编码一样重要
TDD可以减少调试时间
在疲劳的时候应该离开工作
回家路上把自己从工作中抽离出来
洗澡时或许会浮现解决方案
三项法则
优势
练习解决这个问题所需要的动作和决策
学习热键和导航操作、测试驱动开的、持续集成之类的方法
两个人的kata,一个人写单元测试,另一个人写程序
多人参与的wasa
开源
使用自己的时间练习
不要过早精细化需求
迟来的模糊性:需求文档中的模糊之处,都对应着业务方的分歧
“完成”的含义
沟通:开发方、业务方、测试方达成共识
自动化:缩减成本
测试并不是额外工作
验收测试与开发应当不是同一个人编写
开发人员的角色是把验收测试和开发系统联系起来保证测试的通过
不能被动接收测试,需要协商并改进
验收测试和单元测试:内部vs外部
图形界面和其他复杂因素:调用API而不是GUI
持续集成:失败应立刻终止
自动化测试金字塔:
学会拒绝/离席,确定议程和目标,立会,迭代计划会议,争论与反对
凡是不能在五分钟内解决的争论,都不能靠辩论解决。
唯一的出路是:用数据说话。可以做试验、模仿或者建模。
学会安排时间,妥善使用自己的注意力点数
睡眠/咖啡/恢复/肌肉注意力/输入和输出
记录下来并画图展示
优先级错乱——提高某个任务的优先级,有借口推迟真正急迫的任务
不执拗于不容放弃也无法绕开的主意,听取其他意见。越是坚持,浪费的时间越多。
走回头路是最简单的办法。
发现身处泥潭还固执前进,是最严重的优先级错乱。
承诺还是猜测?
预估是一种猜测,不包含任何承诺的色彩。不是一个定数,是一种概率分布。
小心给出暗示性的承诺。
乐观预估(1%),标称预估(概率最大),悲观预估(1%)
得出任务的期望完成时间和任务的概率分布标准差(不确定性)
德尔菲法:
控制错误的方法——把大任务分成小任务,分开预估在加总,结果会比单独评估大任务要准确的多
避免对没有把握能够达成的最后期限做出承诺
让系统、代码和设计尽可能整洁
在危机时也要遵守纪律原则(例如TDD)
不要惊慌失措:深思熟虑,努力寻找可以带来最好结果的方法
沟通:告诉你制定的走出困境的计划,请求支援和指引
坚信纪律和原则
寻求帮助:结对编程
首要职责是满足雇主的需求,和团队协作,深刻理解业务目标,了解企业如何从你的工作中获得回报
拥有代码的是整个团队,而不是个人
专业人士会共同工作,彼此面对面。
形成团队需要时间。建立关系,学会互相协作,了解彼此的癖好和长短处,最后凝聚成为团队。
把项目分配给已经形成凝聚力的团队,而不是围绕项目组建团队。
辅导:
实习生:
技艺:
年前的两个月忙着做实验室的项目,年后开始准备实习的事情。我常说自己足够幸运,在应该做某件事的时候就去做了这件事,这样尽力就好。但这也只是在事后对于自己的些微自嘲而已,我还是改不掉过分焦虑的坏毛病。正如高考前失眠,考研前默背政治到半夜,准备实习的我还是经常会自己一个人默默焦虑地胡思乱想到失眠。
中间经历了其实不算多的面试,因为想要投的岗位对口的其实并不算多。在实验室的经历让我走了一条和别的同学不太一样的岗位,得益于平时的兴趣比较杂比较宽泛,算是很幸运的拿到了实习岗位。其实回顾来看,最后的结果其实是超乎于我自己意料之外的,因为入职后见过身边远远优秀与我的同学太多。
如果对当前的选择感到迷茫,不如找一个能看清方向的人交流,远比自己尝试碰壁或者和同类人无效讨论要有效得多。希望自己能在以后的经历中谨记住这一点。
工作了三四个月,回顾下其实觉得收益到最多的反而是工作方式。很多时候,我们抱怨工作枯燥,内容无趣。但总是会有人能从这些枯燥的工作中提取出更有效的东西,提高自己的效率,提高自己的思考深度,这让我真切体会到平凡和优秀的区别。说和做,差的总是很远。
后面回到学校准备毕设中期,除了完成一篇自己之前觉得有点难度的水论文,倒也没有什么大的波澜。
对于个人生活来说,19年真的是很重要的一年。不同于工作上能够絮絮叨叨说出个所以然,对于我来说,生活是由一个个或难忘或开心或感动,也或是平凡幸福的时刻组成的。很多个这样的时刻会慢慢沉淀成一种潜移默化的东西,支撑自己在各种困难的时候坚持过去。今年这样的时刻格外的多,或许因为不再是一个人的原因吧。
如果说要给自己的2020寄托一些什么希冀一些什么,大概还是一些意识到却没有做到的东西。
其实如果能做到这两点,总感觉2020就会好。但是人总是有点惰性的,我也还是那个对自己有限悲观的人。接下来的一年,能做到一个,也足够让我满足了。毕竟,顺遂心意永远是最重要的。拧巴地生活,还不如维持现状呢。
]]>DesiredStateOfWorldPopulator 和 Reconciler 两个 Goroutine 会通过图中两个的 StateOfWorld 状态进行通信,DesiredStateOfWorldPopulator 主要负责从 Kubernetes 节点中获取新的 Pod 对象并更新 DesiredStateOfWorld 结构;而后者会根据实际状态和当前状态的区别对当前节点的状态进行迁移,也就是通过 DesiredStateOfWorld 中状态的变更更新 ActualStateOfWorld 中的内容。
Ref: https://draveness.me/kubernetes-volume
下面就来分析一下DesiredStateOfWorldPopulator的源码。
DesiredStateOfWorldPopulator的数据结构如下:
1 | type desiredStateOfWorldPopulator struct { |
DesiredStateOfWorldPopulator的接口有三个方法:
1 | type DesiredStateOfWorldPopulator interface { |
除了核心执行方法Run,ReprocessPod能够将特定Pod强制剔出processedPods列表进行强制重新处理。该方法用于在Pod更新上启用重新挂载卷。而HasAddedPods方法则返回populator是否已经将所有现有Pod处理添加到desired state中。
run方法中,每隔loopSleepDuration就会执行一次populatorLoopFunc。
1 | func (dswp *desiredStateOfWorldPopulator) populatorLoopFunc() func() { |
findAndAddNewPods遍历所有Pod并且将“应该添加到期望状态但实际上没有添加”的Pod添加到对应状态值中。
分析流程可知该方法先寻找不是终止状态的Pod,再调用processPodVolumes处理这些符合条件的Pod。
1 | func (dswp *desiredStateOfWorldPopulator) findAndAddNewPods() { |
终止状态判定生效条件满足一条即判定为终止状态:
终止状态判定完毕,核心方法processPodVolumes会将给定Pod中的Volumes进行处理并添加到期望状态值中。
1 | // processPodVolumes processes the volumes in the given pod and adds them to the desired state of the world. |
可以看到,该方法处理流程如下:
其中,步骤3的createVolumeSpec首先会判断该podVolume的Source是否为PVC,如果为PVC则需要找到Claim背后的PV Name,再通过PV Name获取真正的PV对象并返回。如果PVC为空,则对PV深拷贝并创建Spec对象返回。
1 | func (dswp *desiredStateOfWorldPopulator) getPVCExtractPV( |
实际上,通过PVC找PV Name是由KubeClient向API Server请求得到的。请求通过Namespace和Claim Name获取到PVC对象,确认PVC对象的Phase为Bound状态且pvc.Spec.VolumeName不为空。上述流程成功后返回PVC的VolumeName(即PV Name)和该PVC的UID。
获取到pvName和pvcUID后,再次通过KubeClient向API Server请求得到PV对象。请求成功后检查ClaimRef是否为空,ClaimRef的UID和传入的PVC UID是否一致。最后返回该PV对象,在返回的同时一并返回的还有PV的GID。
再看看步骤4,其调用的AddPodToVolume方法如果检查到没有可用的Volume插件或者可用插件不止一个,会返回Error。如果Pod Unique Name重复,则不执行任何操作。此外,如果Volume Name如果不在该节点的Volume列表中,则该Volume会被隐式添加( implicitly added)。
If a volume with the name volumeName does not exist in the list of volumes that should be attached to this node, the volume is implicitly added.
先从desiredStateOfWorld中遍历待挂载的Volume,然后从PodManager中根据待挂载Volume的Pod UID查找该对应Pod。跳过正在运行和不需要删除Volumes(keepTerminatedPodVolumes)的Pod,执行删除流程。
当Pod从PodManager中删除Pod时,Pod不会在Volume Manager中立即删除,需要确认kubelet容器运行时所有的Container已经全部终止。此外,同时还要确认actualStateOfWorld缓存中是否存在待挂载Volume信息。
上述确认过程确认完毕后,从desiredStateOfWorld缓存中删除Pod,表明指定的Pod不再需要该Volume。同时,从dswp维护的processedPods列表中删除该Pod。
]]>1 | public class Example { |
首先,给出一张手绘的流程图。
首先是创建ApiClient对象,从defaultClient方法切入,它返回的是一个由ClientBuilder的standard方法创建的ApiClient对象。
1 | public static ApiClient defaultClient() throws IOException { |
继续追踪ClientBuilder中Standard方法的源码,代码注释显示该方法会通过四种预先配置好的方式中的一种创建一个builder,优先度顺序如下:
$HOME/.kube/config
可以被找到,则使用该配置;In-cluster Service Account
能被找到的话,则它就承担集群配置的功能;localhost:8080
作为最后的方法。如果配置文件或者对应的配置无效的话,会抛出ConnectException异常。下来根据代码来验证以上的注释内容:
1 | public static ClientBuilder standard(boolean persistConfig) throws IOException { |
可以看到代码中实现与注释一一对应,在第一二种配置方式中,核心的方法主要有KubeConfig的loadKubeConfig和setPersistConfig两个方法。从字面上来看,前者主要负责配置加载,而后者则是对于PersistConfig的设置,具体是什么后面再看。
首先分析loadKubeConfig方法:
1 | public static KubeConfig loadKubeConfig(Reader input) { |
从KubeConfig类的变量声明能看到一些额外的信息:
1 | // 找到kubeconfig文件的默认地址相关字段 |
standard方法中,在执行完loadKubeConfig对象之后,会对传入的persistConfig标志位进行判断,如果为true,则执行setPersistConfig方法:
1 | if (persistConfig) { |
而FilePersister是ConfigPersister接口的一个具体实现,该接口仅有一个save方法,应当是将配置持久化的方法。
1 | public interface ConfigPersister { |
standard方法最后的执行过程是kubeconfig方法,方法的注释说明该方法用于从一个预配置好的KubeConfig对象中创建builder,具体源码如下:
1 | public static ClientBuilder kubeconfig(KubeConfig config) throws IOException { |
经历以上过程,终于standard方法执行完毕并返回了一个包含各种认证鉴权相关信息的builder对象,接下来看看build方法的实现。
1 | public ApiClient build() { |
经历以上步骤,终于完成了ApiClient复杂的创建过程。继续看看看看剩余工作,如下:
1 | #创建client对象 |
经历以上的分析过程,大致完成了list pods的流程分析。结合K8S官方的概念流程文档可以验证之前学习的基于API Server的安全机制流程,可以看到Authentication和Authorization部分的数据填充过程。
]]>1 | # 文件名:hello.go |
补充:
编译运行
(仅记录与Java不同的)
常量声明 const、变量定义 var、函数定义 func、延迟执行 defer、 结构类型定义 struct、通道类型 chan
类型 | 标识 |
---|---|
整型 | byte int int8-int64 uint uint8-uint64 uintptr (byte就是uint8) |
浮点数 | float32 float64 (自动类型推断为float64) |
复数 | complex64 complex128 (由两个float构成,对应实部和虚部) |
字符 | rune |
接口 | error |
连续枚举类型 | iota |
匿名变量 | _ |
iota用法: iota用于常量声明中,初始值为0,逐行增加
1 | const ( |
注意:Go语言里自增和自减是语句而不是表达式[1]
1 | //显式声明,value可以是表达式,不指定则初始化为类型零值,声明后立即分配空间 |
切片(可变数组)维护三个元素——指向底层数组的指正、切片元素数量、底层数组容量
创建方式:数组索引、make
Go内置的map不是并发安全的,需要时用sync包内的map保证并发安全
map键值对的修改不能通过map引用直接修改键值,需要KV整体赋值
1 | if initialization; condition { |
【1】这是否意味着自增或自减是原子操作?
答:不是。
API Server作为集群控制请求的实际入口,通常暴露了两个端口——本地端口(Localhost Por)和安全端口(Secure Port)。
本地端口 | 安全端口 | |
---|---|---|
使用场景 | 测试和启动时使用,Master节点中不同组件通信 | 任意场景 |
安全协议 | 无 | TLS |
端口 | 默认8080, | |
insecure-port | ||
修改 | 默认6443, | |
secure-port | ||
修改 | ||
IP | 默认localhost, | |
insecure-bind-address | ||
修改 | 默认为第一个非localhost网卡地址, | |
bind-address | ||
修改 | ||
处理流程 | 无需认证和授权 | 需要认证和授权 |
准入控制 | 是 | 是 |
访问控制 | 需要拥有主机访问权限 | 需要认证和授权模块正常运行 |
注:TLS中,证书和私钥相关参数为tls-cert-file
和 tls-private-key-file
。
访问API Server的方式有kubectl,客户端的库和Rest请求。通常来说,想在外部访问API Server需要通过安全端口访问。通过安全端口访问需要经过三重校验,即Authentication(身份认证),Authorization(授权)和Admission Control(准入控制)。
Authentication is the act of confirming the truth of an attribute of a single piece of data claimed true by an entity.
From: https://en.wikipedia.org/wiki/Authentication
通俗的来说,身份认证解决的是“让系统/服务端知道你是谁”的问题,即对用户身份的确认。值得注意的是,Wikipedia中还特别标注“ Not to be confused with authorization. ”
对于身份的认证,现实生活中可能是查验证件,也可能是对暗号。
对应的,在Kubernetes中,也有对应的几种客户端身份认证方式:
可以同时指定多个身份认证模块,在流程中将会以顺序执行的方式进行认证过程,直到其中一个认证模块认证成功。如果请求认证失败,则会返回401状态码。(PS:这里引入了一个401状态码的历史遗留问题——401的语义其实应该是Unauthenticated)
一旦认证成功,用户就会被分配一个特定的username。在随后的访问控制流程中,这个username将会一直使用。尽管如此,这个username却也不会对应存在一个真实的用户对象,该信息也不会被存储。
(我的理解:这个所谓的username的存在仅仅是为了在整个访问控制流程中能够进行上下文信息的传递,完成一个链式的验证。和传统意义上的用户相比,K8S的访问控制基于单次的请求,在请求的过程中抽象来决定行为的合法性和有效性。)
Authorization is the function of specifying access rights/privileges to resources, which is related to information security and computer security in general and to access control in particular.
From: https://en.wikipedia.org/wiki/Authorization
与身份认证不同的是,授权关注的是对资源的访问控制,通俗的说就是“系统要知道你这个身份能够做什么”。
当请求通过了身份认证之后,请求才会进入授权流程。请求的内容包含三个部分:用户名(username of the requester),动作( requested action),动作影响的对象(the object affected by the action)。在已有的多种授权策略中,只要有一个能够声明此用户有执行对应动作的权限,则请求就被授权成功。若所有授权策略全部失败,则返回403状态码。
授权模块的种类:
通过认证和授权流程之后,请求的调用还需要通过准入控制链的检查。与上述模块不同,准入控制能够修改请求参数完成一些任务。
当多个准入控制模块配置完毕后,请求的调用会依次按顺序进行检查。一旦任意一个准入控制模块检查不通过,则请求立即被拒绝。而请求完成了所有检查后,会采用相应API对象的验证流程对请求进行验证,然后写入对象库。
Once a request passes all admission controllers, it is validated using the validation routines for the corresponding API object, and then written to the object store (shown as step 4).
从整体来看,Kubernetes对于API Server的请求主要分为两大部分:内部和外部。
内部是指在master节点使用kubectl命令进行操作,这时,因为是在节点内部操作,因此并不会使用安全端口,直接采用localhost这个ip上的非安全端口进行访问。
而对于API Server的外部请求调用(包括Pod和Rest请求),则需要使用安全端口进行访问。通过安全端口的请求,需要进行严格的三层校验才能调用成功。这就通过确保只执行权限内允许的操作保证了集群操作的安全性。
关于每个部分的详细介绍,会单独抽成三部分继续分解。
效果:在集群内始终维持用户指定的副本数量
使用:spec.replicas
原理:系统自动调度算法。由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。
效果:通过Node的标签和Pod的nodeSelector属性进行匹配,将Pod调度到指定的Node上
使用:
为目标Node打标签
1 | kubectl label nodes <node-name> <label-key>=<label-value> |
在Pod定义中加上nodeSelector的设置
1 | #pod.yaml |
补充:
篇幅原因,另外一篇单独记录
效果:Pod无法在标记了Taint属性的节点上运行, 同时,设置了Tolerations的Pod可以运行在标注了Taint的Node上
使用:
为Node设置Taint信息
1 | kubectl taint nodes node1 key1=value1:NoSchedule |
在Pod的配置文件中配置tolerations属性
1 | tolerations: |
补充:
效果:在每个Node上调度运行同一个(种)Pod,例如日志采集、性能监控、存储的Daemon进程
使用:
1 | apiVersion: extensions/v1beta1 |
补充:
除了使用系统内置算法在每台Node上调度外,也可以在Pod定义中使用NodeSelector或者NodeAffinity来指定满足料件的Node范围进行调度
效果:并行/串行启动多个计算进程去处理一批工作项(Work Item,下称WI),处理完成后,批处理任务结束
任务模式分类:
Job分类:
(个人理解:上述的规则说明其实是在说所有Pod表现为同一整体,Pod启动失败会重启是一种容错机制。然而从整个过程的跨度来看,无需关心失败启动的数目,只要不是所有Pod全部失败结束,只需存在一个成功结束的Pod即表明Job流程内的其他划分任务都正常完成,整体任务也已成功完成。)
效果:定期触发任务执行
使用:
在API Server启动进程上添加配置参数
1 | --runtime-config=batch/v2alpha1=true |
编写Cron Job配置文件
1 | #cron.yaml |
schedule格式如下
1 | Min Hour DayOfMonth Month DayOfWeek |
*表示任意值,即每个时间单元节点都会触发
/表示开始触发的时间,例如5/20,表明第一次触发在第5个时间单位,此后每隔20个时间单位触发
在Pod中提供自定义的调度器名称,则默认调度器就会失效,转而使用指定的调度器完成对应Pod 的调度,自定义的调度器需要通过kube-proxy来运行,如果自定义调度器始终未启动,则Pod将会卡Pending状态。
1 | apiVersion: v1 |
OP的反域名劫持保护在默认情况是开开启的,具体设置在/etc/config/dhcp
下。
1 | config dnsmasq |
在反域名劫持保护未关闭的情况下,由于上级dns返回的地址是个私有局域网地址,所以被看作是一次域名劫持,从而丢弃了解析的结果。
直接的方法就是将上面的字段值改为0
,关闭即可。
在GUI配置界面等同于将Network->DHCP DNS-Server Settings->General Settings->Rebind protection
的勾取消掉。
再仔细查看文档发现也可以通过白名单的方式放行想要解析的内网域名,更为安全,此时Rebind protection也是处于开启状态,上面的关闭操作不需要进行。而具体修改的操作示例如下所示:
1 | config dnsmasq |
表示在反域名劫持保护情况下,将bupt.edu.cn
,byr.cn
域名加入白名单,允许返回内网地址。
在GUI配置界面等同于在Network-DHCP DNS-Server Settings-General Settings-Domain whitelist
添加想要解析的内网域名。
在学校DNS偶尔抽风或者速度慢的情况下,产生了自定义DNS的想法。由于教务系统等系统的访问需要,在各个客户端修改hosts略显麻烦,并且DNSmasq亦可以实现广告屏蔽,因此采用DNSmasq来实现不同的DNS解析。
预期需求为bupt.edu.cn
, byr.cn
域名使用校内DNS解析,其他地址使用公共DNS解析(以114为例)。
Wan口DNS主要控制路由器访问网络使用的DNS服务器。例如,路由器安装软件需要访问网络,那么所使用的DNS服务器就是这个。
在/etc/config/network
文件中的wan接口添加两行peerdns以及dns字段:
1 | config interface 'wan' |
重启network服务后生效。
在GUI配置界面等同于在Network-Interface-Wan-Edit-Common Configuration-Advanced Settings
中取消Use DNS servers advertised by peer
的勾选,并在Use custom DNS servers
添加默认的DNS服务器。
LAN口DNS主要控制连接到路由器的设备使用的DNS。例如,连到路由的电脑上网时使用的DNS服务器就在这里设置。
一般情况下,Lan和Wan口DNS保持一致即可。如若有需要,修改/etc/config/dhcp
文件中dnsmasq的resolvfile指向即可:
1 | config dnsmasq |
同时需要在/etc/resolv.dnsmasq.conf
下新建对应的配置文件。示例如下:
1 | nameserver 114.114.114.114 |
接下来就是配置校内域名使用的DNS解析地址。修改/etc/config/dhcp
文件中dnsmasq。
首先是删除下面两行配置:
1 | config dnsmasq |
其次添加list server字段,对bupt.edu.cn
, byr.cn
相关域名使用校内DNS解析:
1 | config dnsmasq |
在GUI配置界面等同于在Network-DHCP and DNS-Server Settings-General Settings-DNS forwardings
添加对应域名的DNS解析服务器地址。
DNSmasq的用法远不止于此,可用来内网域名IP映射代替hosts,自定义域名解析规则屏蔽广告等,有时间会再研究。
]]>1 | dest root / |
注:软件源由于硬件配置不同的会有所区别,Newifi是MT7620方案,其他芯片方案的请移步以下两个网址自行匹配:
OpenWrt中文网址 http://downloads.openwrt.org.cn/
OpenWrt download area https://downloads.openwrt.org/
1 | config dnsmasq |
修改后保存并重启路由器即可。
配置后好像dns出了一些问题,在访问其他校内以.byr.cn或.bupt.edu.cn为后缀的网址显示dns错误,如果有大牛解决了这个DNS问题,可以分享一下思路。
2018.9.9更新:后话所述问题已经解决
]]>