稀疏特征和密集特征是机器学习和深度学习中常见的两种特征类型,它们有不同的存储方式和处理方法。
在机器学习中,特征是指对象、人或现象的可测量和可量化的属性或特征。特征可以大致分为两类:稀疏特征和密集特征。
稀疏特征(Sparse Feature) 指的是特征值大部分为0的特征,例如文本数据中的词频、one-hot向量等。对于稀疏特征,我们通常使用稀疏矩阵(Sparse Matrix)来存储,只存储非0的元素和它们的索引,可以大大节省存储空间和计算资源。在深度学习中,我们也可以使用Embedding层来对稀疏特征进行编码,将高维稀疏向量映射为低维稠密向量,以便进行神经网络的训练和推理。
密集特征(Dense Feature) 指的是特征值大部分为非0的特征,例如图像数据中的像素值、音频数据中的频谱、时间序列数据中的数值等。对于密集特征,我们通常使用密集矩阵(Dense Matrix)来存储,每个元素都有一个实数值。在深度学习中,我们通常使用全连接层(Dense层)来对密集特征进行编码,将输入特征向量映射为输出特征向量,以便进行神经网络的训练和推理。
区别
稀疏特征和密集特征之间的区别在于它们的值在数据集中的分布。稀疏特征具有很少的非零值,而密集特征具有许多非零值,这种分布差异对机器学习算法有影响,因为与密集特征相比,算法在稀疏特征上的表现可能不同。
需要注意的是,稀疏特征和密集特征并不是互相独立的,实际的数据集通常包含多种类型的特征,其中一些特征可能是稀疏的,一些特征可能是密集的,甚至还可能包含序列、图像、音频等多种类型的数据。在处理这些数据时,我们需要根据不同的特征类型选择合适的存储方式和处理方法,以便提高模型的效率和准确率。
算法选择
现在我们知道了给定数据集的特征类型,如果数据集包含稀疏特征或数据集包含密集特征,我们应该使用哪种算法?
一些算法更适合稀疏数据,而另一些算法更适合密集数据。
但需要注意的是,算法的选择不仅仅取决于数据的稀疏性或密度,还应考虑数据集的大小、特征类型、问题的复杂性等其他因素 ,一定要尝试不同的算法并比较它们在给定问题上的性能。
dense 表示稠密,在embedding中的dense时:
假设我们有这样一个句子: “北京是北京”,我们将其数值化表示为:
dense embedding,需要你讲它转换成onehot表示:
假设embedding对输出size=3,也就是hidden层的size=3*3;
eg:
那么dense layer的计算过程就是一个矩阵相乘:
整个流程展开来看就是:
你会看到这个过程:
那么有没有方法,优化一下这两个问题(计算量大,输入尺寸也大)呢?
sparse : 表示稀疏,在embedding中的dense时:
同样假设我们有这样一个句子: “北京是北京”,我们将其数值化表示为:
sparse embedding,不需要你转换乘onehot编码格式:
那么,它是如何计算的呢?
假设embedding对输出size=3,也就是hidden层的size=3*3;
eg:
那么sparse layer的计算过程的“矩阵相乘”(相当于一个查表的过程,所以有lookup_table这个表述):
这个计算过程为:
最终得到:
你会看到,dense和sparse结果都一样,但是这个计算量变成列O((N1)(M*M)) 减少列一个量级. 而且输入input的vec也极大的缩小了,毕竟存储的是index嘛.
那么会到我们开始的问题,NN[神经网络]中embedding的dense和sparse是什么意思?
结合上面的例子的计算过程,dense embedding 就是要求输入必须为onehot,sparse embedding 不需要.
那么在扩大一点,NN[神经网络]中的dense和sparse是什么意思?
dense和sparse描述的是该层hidden layer和前后层的网络连接情况,如果hidden layer 和前一层以及后一层参数连接多,我们就说他是dense layer,比如全连接层(fc),相反,如果连接数比较少,我们说它是sparse layer。
]]>from typing import Anyimport time'''函数装饰器'''def timeit(f): def wapper(*args, **kwargs): start_time = time.time() ret = f(*args, **kwargs) print(time.time() - start_time) return ret return wapper@timeitdef my_func(x): time.sleep(x)@timeitdef orther(x, y): return x * y# # my_func = timeit(my_func)myfunc(3)# orther = timeit(orther)print(orther(2, 3))'''类装饰器__call__方法是python魔法方法的一种,它的作用是将类的实例化对象变成可调用对象,类似于像函数一样被调用。'''class Timer: def __init__(self, func) -> None: self.func = func def __call__(self, *args: Any, **kwargs: Any) -> Any: start_time = time.time() ret = self.func(*args, **kwargs) print(f'Time: {time.time() - start_time}') return ret'''带参数的类装饰器'''class Timer: def __init__(self, prefix) -> None: self.prefix = prefix def __call__(self, func): def wapper(*args, **kwargs): start_time = time.time() ret = func(*args, **kwargs) print(f'{self.prefix}{time.time() - start_time}') return ret return wapper@Timer@Timer(prefix="current_time: ")def add(x, y): return x + y# 不带参数的类装饰器, 等价于add = Timer(add)# 带参数的类装饰器, 等价于add = Timer(prefix="current_time: ")(add)print(add(2, 3))
# if 后面的执行条件是可以简写的,只要条件 是非零数值、非空字符串、非空 list 等,就判断为 True,否则为 False。if a: print('a 为非空')if b is None: pass # 字符串拼接print(f'Wow {name}! you have {subscribers} subscribers!') # dict get key# 拿不到key默认就是Nonedict.get('fault_task_id')# 列表生成式num = [i for i in range(10)]# 类型断言a = []if isinstance(a, list): pass# with# forfor i in range(1, 10): # 从1开始遍历 print(i)# 列表a = [1, 2, 3]b = [4, 5, 6]for index, value in enumerate(a): # 1. 遍历索引以及value passfor av, bv in zip(a,b): # 2. 遍历两个列表 passa = [i for i in range(10)] # 3. 简洁操作列表 # 字典d = {"a":1, "b":2}t = {"a":1, "c":3}for k in d: # 1. 遍历key passfor k, v in d.items(): # 2. 遍历key, value passmerge_dict = {**d, **t} # 3. 合并字典 # joinmy_list = ['Hello', 'my', 'friends']my_str = " ".join(my_list)print(my_str) # 优雅你的判断语句x = -6y = -x if x<0 else x# Be consistent in return statements. Either all return statements in a function should return an expression, or none of them should. If any return statement returns an expression, any return statements where no value is returned should explicitly state this as return None, and an explicit return statement should be present at the end of the function (if reachable)# Wrong:def foo(x): if x >= 0: return math.sqrt(x)def bar(x): if x < 0: return return math.sqrt(x) # Correct:def foo(x): if x >= 0: return math.sqrt(x) else: return Nonedef bar(x): if x < 0: return None return math.sqrt(x)# 多条件内容判断至少一个成立math,English,computer =90,59,88if any([math<60,English<60,computer<60]): print('not pass')# filterdef func(a): return a % 2 != 0print(filter(func, range(10)))for i in filter(func, range(10)): print(i)'''<filter object at 0x7f1f4840a198>13579'''# 元组比列表占用的内存要小很多# rasik
'''1. __new__(少): 从class建立object的过程2. __init__: 有了object后, 给object初始化的过程ex:obj = __new__(ClassA)__init__(obj)dir(obj): 列出对象可调用的所有属性20. __getitem__: 使用方括号[]读取值的时候ex:class A: def __init__(self, data) -> None: self._data = data def __getitem__(self, key): return self._data[key]a = A([1, 2])print(a[1])21. __setitem__: 使用[]设置值的时候14. __getattr__: 只有在读取一个不存在的属性时会调用ex:class A: def __getattr__(self, attr): print(f'{attr} not exsit') raise AttributeError15. __getattribute__(少): 只要读取属性, 都会调用16. __setattr__: 在设置属性的时候会调用ex:class A: def __init__(self, data) -> None: self._data = data def __getitem__(self, key): return self._data[key] def __setitem__(self, key, value): self._data[key] = valuea = A([1, 2])a[1] = "Any"print(a[1])17. __delattr__(少): 尝试删除对象属性的时候调用18. __len__(少): 内置函数len调用的时候, 会调用对象的__len__函数3. __del__(少): 当这个对象被释放的时候, 要干点什么4. __repr__(少): 返回这个object的字符串表示, 一般为注释, 详细版5. __str__: 返回这个object的字符串表示, 一般为注释, 简易版6. __format__(少)7. __bytes__(少)8. __eq__(少): 比较函数, 等于9. __ne__(少): 比较函数, 不等于10. __gt__(少)11. __lt__(少)12. __ge__(少)13. __le__(少)'''
'''Python中的super()函数是一个非常有用的工具,它可以用来调用父类的方法和属性。下面是一些使用场景的例子'''# 在子类中调用父类的构造方法。在子类中,如果您想继承父类的构造方法,但又想添加自己的额外操作,# 您可以使用super()函数来调用父类的构造方法,并执行您自己的操作。例如:class SubClass(SuperClass): def __init__(self, arg1, arg2, arg3): super().__init__(arg1, arg2) self.arg3 = arg3# 在这个例子中,子类SubClass继承了父类SuperClass的构造方法,并在自己的构造方法中添加了一个额外的参数arg3。# 在子类中调用父类的方法。如果您想在子类中调用父类的方法,您可以使用super()函数来实现。例如:class SubClass(SuperClass): def some_method(self, arg): super().some_method(arg) # do some other stuff here# 在这个例子中,子类SubClass继承了父类SuperClass的方法some_method(),并在自己的方法中添加了一些其他的操作。# 在多重继承中,使用super()函数来调用正确的父类方法。在多重继承的情况下,子类可能继承了多个父类,# 这时使用super()函数可以帮助您调用正确的父类方法。例如:class SubClass(SuperClass1, SuperClass2): def some_method(self, arg): super(SuperClass1, self).some_method(arg) super(SuperClass2, self).some_method(arg)# 在这个例子中,子类SubClass继承了两个父类SuperClass1和SuperClass2,并在自己的方法中使用super()函数分别调用这两个父类的方法。
@classmethod 装饰的类方法,也可以是 @staticmethod 装饰的静态方法
@classmethod : 因为持有cls参数,可以来调用类的属性,类的方法,实例化对象等,避免硬编码。
@staicmethod : 相当于定义了一个局部域函数为该类专门服务,没什么其它用处吧。
class A(object): def m1(self, n): print("self:", self) @classmethod def m2(cls, n): print("cls:", cls) @staticmethod def m3(n): pass
# 在Python代码执行时,Python解释器首先将源代码解析成抽象语法树,然后将其编译成Python字节码,# 最后执行字节码。 Python字节码是一种基于堆栈的指令集,其中每个指令都会对堆栈进行操作。# # Python 代码在底层究竟是如何工作的1. 刚开始运行 python时, 会建立一个新的 frame.2. 在这个 frame的环境下, 会一条一条执行 bytecode.3. 每一条 bytecode在 c语言里有相应的代码去执行他.4. 在每一个 frame里面, pyhton 会维护一个 stack(栈).5. bytecode 会跟 stack(栈)进行交互, 进行结算, 拿到结果返回, 继续循环.
SLO和SLA是大家常见的两个名词:服务等级目标和服务等级协议。
云计算时代,各大云服务提供商都发布有自己服务的SLA条款,比如Amazon的EC2和S3服务都有相应的SLA条款。这些大公司的SLA看上去如此的高达上,一般是怎么定义出来的呢?本文就尝试从技术角度解剖一下SLA的制定过程。
说SLA不能不提SLO,这个是众所周知的,但是还有一个概念知道的人就不多了,那就是SLI(Service Level Indicator),定义一个可执行的SLA,好的SLO和SLI是必不可少的。
再有就是SLI/SLO/SLA都是和服务联系在一起的,脱离了服务这三个概念就没有什么意义了。
什么是服务?
简单说就是一切提供给客户的有用功能都可以称为服务。
服务一般会由服务提供者提供,提供这个有用功能的组织被称为服务提供者,通常是人加上软件,软件的运行需要计算资源,为了能对外提供有用的功能软件可能会有对其他软件系统的依赖。
客户是使用服务提供者提供的服务的人或公司。
SLI是经过仔细定义的测量指标,它根据不同系统特点确定要测量什么,SLI的确定是一个非常复杂的过程。
SLI的确定需要回答以下几个问题:
要测量的指标是什么?
测量时的系统状态?
如何汇总处理测量的指标?
测量指标能否准确描述服务质量?
测量指标的可靠度(trustworthy)?
1.常见的测量指标有以下几个方面:
性能
响应时间(latency)
吞吐量(throughput)
请求量(qps)
实效性(freshness)
可用性
运行时间(uptime)
故障时间/频率
可靠性
质量
准确性(accuracy)
正确性(correctness)
完整性(completeness)
覆盖率(coverage)
相关性(relevance)
内部指标
队列长度(queue length)
内存占用(RAM usage)
因素人
响应时间(time to response)
修复时间(time to fix)
修复率(fraction fixed)
下面通过一个例子来说明一下:hotmail的downtime SLI
错误率(error rate)计算的是服务返回给用户的error总数
如果错误率大于X%,就算是服务down了,开始计算downtime
如果错误率持续超过Y分钟,这个downtime就会被计算在内
间断性的小于Y分钟的downtime是不被计算在内的。
2.测量时的系统状态,在什么情况下测量会严重影响测量的结果
测量异常(badly-formed)请求,还是失败(fail)请求还是超时请求(timeout)
测量时的系统负载(是否最大负载)
测量的发起位置,服务器端还是客户端
测量的时间窗口(仅工作日、还是一周7天、是否包括计划内的维护时间段)
3.如何汇总处理测量的指标?
计算的时间区间是什么:是一个滚动时间窗口,还是简单的按照月份计算
使用平均值还是百分位值,比如:某服务X的ticket处理响应时间SLI的
测量指标:统计所有成功解决请求,从用户创建ticket到问题被解决的时间
怎么测量:用ticket自带的时间戳,统计所有用户创建的ticket
什么情况下的测量:只包括工作时间,不包含法定假日
用于SLI的数据指标:以一周为滑动窗口,95%分位的解决时间
4. 测量指标能否准确描述服务质量?
性能:时效性、是否有偏差
准确性:精度、覆盖率、数据稳定性
完整性:数据丢失、无效数据、异常(outlier)数据
5. 测量指标的可靠度
是否服务提供者和客户都认可
是否可被独立验证,比如三方机构
客户端还是服务器端测量,取样间隔
错误请求是如何计算的
SLO(服务等级目标)指定了服务所提供功能的一种期望状态。SLO里面应该包含什么呢?所有能够描述服务应该提供什么样功能的信息。
服务提供者用它来指定系统的预期状态;开发人员编写代码来实现;客户依赖于SLO进行商业判断。SLO里没有提到,如果目标达不到会怎么样。
SLO是用SLI来描述的,一般描述为:
比如以下SLO:
每分钟平均qps > 100k/s
99% 访问延迟 < 500ms
99% 每分钟带宽 > 200MB/s
设置SLO时的几个最佳实践:
指定计算的时间窗口
使用一致的时间窗口(XX小时滚动窗口、季度滚动窗口)
要有一个免责条款,比如:95%的时间要能够达到SLO
如果Service是第一次设置SLO,可以遵循以下原则
测量系统当前状态
设置预期(expectations),而不是保证(guarantees)
初期的SLO不适合作为服务质量的强化工具
改进SLO
保持一定的安全缓冲
不要超额完成
设置SLO时的目标依赖于系统的不同状态(conditions),根据不同状态设置不同的SLO:总SLO = service1.SLO1 weight1 + service2.SLO2 weight2 + …
为什么要有SLO,设置SLO的好处是什么呢?
对于客户而言,是可预期的服务质量,可以简化客户端的系统设计
对于服务提供者而言
可预期的服务质量
更好的取舍成本/收益
更好的风险控制(当资源受限的时候)
故障时更快的反应,采取正确措施
SLO设好了,怎么保证能够达到目标呢?
需要一个控制系统来:
监控/测量SLIs
对比检测到的SLIs值是否达到目标
如果需要,修证目标或者修正系统以满足目标需要
实施目标的修改或者系统的修改
该控制系统需要重复的执行以上动作,以形成一个标准的反馈环路,不断的衡量和改进SLO/服务本身。
我们讨论了目标以及目标是怎么测量的,还讨论了控制机制来达到设置的目标,但是如果因为某些原因,设置的目标达不到该怎么办呢?
也许是因为大量的新增负载;也许是因为底层依赖不能达到标称的SLO而影响上次服务的SLO。这就需要SLA出场了。
SLA是一个涉及2方的合约,双方必须都要同意并遵守这个合约。当需要对外提供服务时,SLA是非常重要的一个服务质量信号,需要产品和法务部门的同时介入。
SLA用一个简单的公式来描述就是: SLA = SLO + 后果
SLO不能满足的一系列动作,可以是部分不能达到
对动作的具体实施
SLA是一个很好的工具,可以用来帮助合理配置资源。一个有明确SLA的服务最理想的运行状态是:增加额外资源来改进系统所带来的收益小于把该资源投给其他服务所带来的收益。
一个简单的例子就是某服务可用性从99.9%提高到99.99%所需要的资源和带来的收益之比,是决定该服务是否应该提供4个9的重要依据。
]]>故障情况:
阿里云账号A的A机房,内网里面部署两台Nginx,通过网络出口(NAT),代理用户访问到阿里云账号B的B机房服务。A机房的Nginx出现:upstream timed out 。
故障的诱因是:net.ipv4.tcp_timestamps=1
抓包图:
注意,这个选项生效的前提是,报文的发出方必须在TCP头部的可选项中增加时间戳字段,否则这个设置是不生效的。
直接上当年的笔记:
先看看TCP IP 对tw的一些解析:
RFC 1323里有这样的定义:
TCP Extensions for High PerformanceVim
An additional mechanism could be added to the TCP, a per-host
cache of the last timestamp received from any connection.
This value could then be used in the PAWS mechanism to reject
old duplicate segments from earlier incarnations of the
connection, if the timestamp clock can be guaranteed to have
ticked at least once since the old connection was open. This
would require that the TIME-WAIT delay plus the RTT together
must be at least one tick of the sender's timestamp clock.
Such an extension is not part of the proposal of this RFC.
大概的中文意思就是:TCP协议中有一种机制,缓存了每个主机(即ip)过来的连接最新的timestamp值。这个缓存的值可以用于PAWS(Protect Against Wrapped Sequence numbers,是一个简单的防止重复报文的机制)中,来丢弃当前连接中可能的旧的重复报文。而Linux实现这个机制的方法就是同时启用net.ipv4.tcp_timestamps和net.ipv4.tcp_tw_recycle 这两个选项。
这种机制在 客户端-服务器 一对一的时候,没有任何问题,但是当服务器在负载均衡器后面时,由于负载均衡器不会修改包内部的timestamp值,而互联网上的机器又不可能保持时间的一致性,再加上负载均衡是会重复多次使用同一个tcp端口向内部服务器发起连接的,就会导致什么情况呢:
负载均衡通过某个端口向内部的某台服务器发起连接,源地址为负载均衡的内部地址——同一假如恰巧先后两次连接源端口相同,这台服务器先后收到两个包,第一个包的timestamp被服务器保存着,第二个包又来了,一对比,发现第二个包的timestamp比第一个还老——客户端时间不一致。服务器基于PAWS,判断第二个包是重复报文,丢弃之。
反映出来的情况就是在服务器上抓包,发现有SYN包,但服务器就是不回ACK包,因为SYN包已经被丢弃了。为了验证这一结果,可以执行netstat -s | grep timestamp 命令,看输出里面passive connections rejected by timestamp 一项的数字变化。
在tcp_ipv4.c中,在接收SYN之前,如果符合如下两个条件,需要检查peer是不是proven,即per-host PAWS检查:
收到的报文有TCP option timestamp时间戳
本机开启了内核参数net.ipv4.tcp_tw_recycle
linux kernel v3.10 net/ipv4/tcp_ipv4.c 1540行C/* VJ's idea. We save last timestamp seen * from the destination in peer table, when entering * state TIME-WAIT, and check against it before * accepting new connection request. * * If "isn" is not zero, this request hit alive * timewait bucket, so that all the necessary checks * are made in the function processing timewait state. */if (tmp_opt.saw_tstamp && tcp_death_row.sysctl_tw_recycle && (dst = inet_csk_route_req(sk, &fl4, req)) != NULL && fl4.daddr == saddr) {if (!tcp_peer_is_proven(req, dst, true)) {NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);goto drop_and_release;}}
解决办法:
tcp_tw_recycle=0 或(和)net.ipv4.tcp_timestamps=0同时从4.10内核开始,官方修改了时间戳的生成机制,所以导致 tcp_tw_recycle 和新时间戳机制工作在一起不那么友好,同时 tcp_tw_recycle 帮助也不那么的大。
此处的时间戳并不是我们通常意义上面的绝对时间,而是一个相对时间。很多情况下,我们是没法保证时间戳单调递增的,比如业务服务器之前部署了NAT,LVS等情况。相信很多小伙伴上班的公司大概率实用实用各种公有云,而各种公有云的 LVS 网关都是 FullNAT 。所以可能导致在高并发的情况下,莫名其妙的 TCP 建联不是那么顺畅或者丢连接。
而这也是很多优化文章中并没有提及的一点,大部分文章都是简单的推荐将net.ipv4.tcp_tw_recycle设置为1,却忽略了该选项的局限性,最终造成严重的后果(比如我们之前就遇到过部署在nat后端的业务网站有的用户访问没有问题,但有的用户就是打不开网页)。
]]>思维导图
链接:
内存的基础知识
思维导图
链接:
内存管理的概念
思维导图
链接:
内存覆盖与交换
思维导图
链接:
内存的分配与回收
思维导图
链接:
内存动态分区分配算法
思维导图
思维导图
链接:
具有快表的地址变换机构
思维导图
链接:
两级页表
思维导图
链接:
基本分段存储管理方式
服务网格(Service Mesh)通常用于应用程序的微服务网络以及应用之间的交互.
它的需求包括服务发现、负载均衡、故障恢复、指标收集和监控以及更加复杂的运维需求.
istio是Service Mesh的一种实现,istio包括控制平面
、数据平面
.
每个Pod 中包含2个 Container.一个是业务Container,一个是Envoy.所有进出Pod 的流量,都会由经过 Envoy 处理.
istio的各种功能,就是在Envoy 中实现.
istiod 进程中有各种informer, 会监听K8s 对象,把监听到的变更进行聚合,生成 Envoy配置文件, 然后下发到每个 Envoy中生效.
常用对象介绍:
VirtualService: 可以理解为更高级的K8s Service对象,具备配置路由、流量管理、故障处理、故障注入等功能.
DestinationRule: VirtualService路由生效后,配置应用与请求的策略集.通常定义子集、负载均衡策略、断路器等,供 VirtualService使用.
Gateway: 对外暴露的入口,南北流量走Gateway.
ServiceEntry: 通常用于在istio 服务网格之外的服务,加入到Envoy cluster中.
istio架构
发现模型如下:
Listener Discovery Service(LDS)
简单理解,Listener是Envoy打开的一个监听端口,用于接收来自Downstream(客户端)连接。Envoy可以支持复数个Listener。多个Listener之间几乎所有的配置都是隔离的。Listener配置中核心包括监听地址、Filter链等。
Route Discovery Service(RDS)
Listener可以接收来自下游的连接,Cluster可以将流量发送给具体的上游服务,而Router则决定Listener在接收到下游连接和数据之后,应该将数据交给哪一个Cluster处理.它定义了数据分发的规则.Router中最核心配置包含匹配规则和目标Cluster,此外,也可能包含重试、分流、限流等等.
Cluster Discovery Service(CDS)
在Envoy中,每个Upstream上游服务都被抽象成一个Cluster.Cluster包含该服务的连接池、超时时间、endpoints地址、端口、类型(类型决定了Envoy获取该Cluster具体可以访问的endpoint方法)等等.
Endpoint Discovery Service(EDS)
Envoy 通过 EDS API 可以更加智能地动态获取上游 Endpoint.在 Envoy 中用来获取集群成员.集群成员在 Envoy 的术语中被称为“终端”.
Secret Discovery Service(SDS)
Secret 发现服务,用于在运行时动态获取 TLS 证书.在使用 SDS 后,集中式的 SDS 服务器将证书分发给所有的 Envoy 实例.
Health Discovery Service(HDS,使用较少)
支持管理服务器对其管理的 Envoy 实例进行高效的端点健康发现.发起主动健康检查,单个 Envoy 实例通常会收到 HDS 指令,以检查所有端点的子集(subset).
Aggregated Discover Service(ADS)
Envoy 的设计是最终一致的,将 xDS 所有的协议都聚合到一起,即上文提到的 CDS、EDS、LDS 和 RDS 等.可以通过单一的 gRPC 服务流支持所有的资源类型,借助于有序的配置分发,从而解决资源更新顺序的问题,CDS->EDS->LDS->RDS。
envoy配置如下:
DiscoveryRequest/DiscoveryResponse:
https://github.com/envoyproxy/envoy/blob/release/v1.22/api/envoy/api/v2/discovery.proto
🌊
istio的流量劫持机制
istio Init Container
会修改 Pod 的Iptables,达到流量劫持的效果.
以下命令可以观察:
# 查看Pod Iptables规则nsenter -t $Pid -n iptables-save# 查看listener配置istioctl pc listener -nsidecar toolbox-68f79dd5f8-9mr5g -ojson# 查看route配置istioctl pc route -nsidecar toolbox-68f79dd5f8-9mr5g --name 80 -ojson# 查看cluster、endpointistioctl pc cluster -nsidecar toolbox-68f79dd5f8-9mr5g -ojsonistioctl pc endpoint -nsidecar toolbox-68f79dd5f8-9mr5g -ojson
使用默认的REDIRECT模式来重定向流量,将所有出战流量都重定向到Envoy代理,转发到Envoy的15001端口.
Envoy 0.0.0.0:15001上的监听器接受进出Pod的所有流量,然后将请求移交给虚拟监听器.
每一个虚拟监听器对应一个listener,里面包含对应的router、cluster、endpoint.是通过xDS协议watch K8s Api获取的.
Envoy使用 istio-proxy 用户身份运行, UID为1337,所有从Envoy出去的流量都会直接 return.
如下图:
🧱
⚠️
Envoy在匹配路由规则时,会按照 VirtualService 中的顺序进行匹配,列表中第一规则具有最高优先级.通常把 / 匹配放到最下方,也可以通过匹配Request header或K8s label来避免.
如果服务注册发现使用etcd的话,是无法匹配Envoy规则的,因为请求的是Pod ip,无法匹配domain.
tracing的时候,依赖HTTP header,需要应用程序把当前的HTTP header原封不动带上,继续调用下游,Envoy 才能收到并上报给 jaeger,形成链路追踪.依赖的HTTP header如下:
配置一致性检查,要做一定的监控,确保所有Envoy都收到配置.istioctl ps
查看同步状态.
K8s Endpoint的健康检查,istio完全依赖K8s 的检查机制.
记得为客户端添加超时时间,快速失败,避免客户端傻等.
记得加默认的断路器规则,通过限流保护后端程序.
Service Mesh涉及的网络栈
Cilium 数据平面加速
通过eBPF
技术,不在走内核协议栈,减少kernel处理数据包的开销,提高了性能.
业界使用的效果:百度提升20%,腾讯提升5-10%.具体提升见仁见智,如果数据包很大,提升明显.
]]>在Linux下当我们想强制结束一个程序的时候,我们通常会给它发送一个信号然后该进程捕捉到信号,再然后该进程执行一定操作最终被终止。
信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。
信号 | 值 | 动作 | 说明 |
---|---|---|---|
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发 |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止进程(可以被捕获、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 后台程序从终端中读取数据时触发 |
SIGTTOU | 22,22,27 | Stop | 后台程序向终端中写数据时触发 |
kill pid 与 kill -9 pid的区别
kill pid的作用是向进程号为pid的进程发送SIGTERM(这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。
kill -9 pid 则是向进程号为pid的进程发送 SIGKILL(该信号的编号为9),从本文上面的说明可知,SIGKILL既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法“感知”SIGKILL信号,它在完全无准备的情况下,就被收到SIGKILL信号的操作系统给干掉了,显然,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。在某些情况下(如进程已经hang死,无法响应正常信号),就可以使用 kill -9 来结束进程。
应用程序如何优雅退出?
Linux Server端的应用程序经常会长时间运行,在运行过程中,可能申请了很多系统资源,也可能保存了很多状态,在这些场景下,我们希望进程在退出前,可以释放资源或将当前状态dump到磁盘上或打印一些重要的日志,也就是希望进程优雅退出(exit gracefully)。
监听所有信号
package mainimport ("fmt""os""os/signal")// 监听全部信号func main() {c := make(chan os.Signal)// 监听所有信号signal.Notify(c)fmt.Println("启动了程序")s := <-cfmt.Println("收到信号:", s)}
优雅退出
package mainimport ("fmt""os""os/signal""syscall""time")// 优雅退出go守护进程func main() {c := make(chan os.Signal)// 监听信号signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)go func() {for s := range c {switch s {case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM:fmt.Println("退出:", s)ExitFunc()case syscall.SIGUSR1:fmt.Println("usr1", s)case syscall.SIGUSR2:fmt.Println("usr2", s)default:fmt.Println("其他信号:", s)}}}()fmt.Println("启动了程序")sum := 0for {sum++fmt.Println("休眠了:", sum, "秒")time.Sleep(1 * time.Second)}}func ExitFunc() {fmt.Println("开始退出...")fmt.Println("执行清理...")fmt.Println("结束退出...")os.Exit(0)}
]]>
使用聚合层(Aggregation Layer),用户可以通过附加的 API 扩展 Kubernetes, 而不局限于 Kubernetes 核心 API 提供的功能。 这里的附加 API 可以是现成的解决方案,比如 metrics server, 或者你自己开发的 API。
聚合层在 kube-apiserver 进程内运行。在扩展资源注册之前,聚合层不做任何事情。 要注册 API,你可以添加一个 APIService 对象,用它来 “申领” Kubernetes API 中的 URL 路径。 自此以后,聚合层将把发给该 API 路径的所有内容(例如 /apis/myextension.mycompany.io/v1/…) 转发到已注册的 APIService。
APIService 的最常见实现方式是在集群中某 Pod 内运行 扩展 API 服务器。
kube-apiserver 其实包含三种 APIServer:
AggregatorServer:负责处理 apiregistration.k8s.io 组下的 APIService 资源请求,同时将来自用户的请求拦截转发给 Aggregated APIServer(AA);
KubeAPIServer:负责对请求的一些通用处理,包括:认证、鉴权以及各个内建资源(pod, deployment,service)的 REST 服务等;
ApiExtensionsServer:负责 CustomResourceDefinition(CRD)apiResources 以及 apiVersions 的注册,同时处理 CRD 以及相应 CustomResource(CR)的REST请求(如果对应 CR 不能被处理的话则会返回404),也是 apiserver Delegation 的最后一环;
除了聚合 API,官方还提供了另一种方式以实现对标准 kubernetes API 接口的扩展:CRD(Custom Resource Definition ),能达到与聚合 API 基本一样的功能,而且更加易用,开发成本更小,但相较而言聚合 API 则更为灵活。针对这两种扩展方式如何选择,官方也提供了相应的参考。
通常,如果存在以下情况,CRD 可能更合适:
定制资源的字段不多;
你在组织内部使用该资源或者在一个小规模的开源项目中使用该资源,而不是在商业产品中使用;
聚合 API 可提供更多的高级 API 特性,也可对其他特性进行定制;例如,对存储层进行定制、对 protobuf 协议支持、对 logs、patch 等操作支持。
两种方式的核心区别是定义 api-resource 的方式不同。在 Aggregated APIServer 方式中,api-resource 是通过代码向 API 注册资源类型,而 Custom Resource 是直接通过 yaml 文件向 API 注册资源类型。
CRD 是让 kube-apiserver 认识更多的对象类别(Kind),Aggregated APIServer 是构建自己的 APIServer 服务。虽然 CRD 更简单,但是缺少更多的灵活性,更详细的 CRDs 与 Aggregated API 的对比可参考官方文档。
Metrics-server 是 K8s监控体系中的核心组件之一,它负责从kubelet 收集资源指标, 然后对这些指标监控数据进行聚合(依赖kube-aggregator), 并在K8s Apiserver中通过Metrics API(/apis/metrics.k8s.io/)公开暴露它们, 但是Metrics-server 只存储最新的指标数据.
kubectl具体执行过程
GET https://kubemaster.cluster:6443/apis/metrics.k8s.io/v1beta1/nodes/k8smaster01-application.ali
[root@k8smaster01-application ~]# kubectl top node k8smaster01-application.ali -v 9I0807 15:42:25.647138 13405 loader.go:359] Config loaded from file /root/.kube/configI0807 15:42:25.648877 13405 round_trippers.go:419] curl -k -v -XGET -H "Accept: application/json, */*" -H "User-Agent: kubectl/v1.14.7 (linux/amd64) kubernetes/8fca2ec" 'https://kubemaster.cluster:6443/api?timeout=32s'I0807 15:42:25.692029 13405 round_trippers.go:438] GET https://kubemaster.cluster:6443/api?timeout=32s 200 OK in 43 millisecondsI0807 15:42:25.692053 13405 round_trippers.go:444] Response Headers:I0807 15:42:25.692060 13405 round_trippers.go:447] Content-Length: 136I0807 15:42:25.692064 13405 round_trippers.go:447] Date: Sun, 07 Aug 2022 07:42:25 GMTI0807 15:42:25.692069 13405 round_trippers.go:447] Audit-Id: 9d4ed495-fb82-4cc5-bdc8-fff4e5a0eb6bI0807 15:42:25.692073 13405 round_trippers.go:447] Content-Type: application/jsonI0807 15:42:25.692104 13405 request.go:942] Response Body: {"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0","serverAddress":"172.26.25.137:6443"}]}I0807 15:42:25.692363 13405 round_trippers.go:419] curl -k -v -XGET -H "Accept: application/json, */*" -H "User-Agent: kubectl/v1.14.7 (linux/amd64) kubernetes/8fca2ec" 'https://kubemaster.cluster:6443/apis?timeout=32s'I0807 15:42:25.714723 13405 round_trippers.go:438] GET https://kubemaster.cluster:6443/apis?timeout=32s 200 OK in 22 millisecondsI0807 15:42:25.714745 13405 round_trippers.go:444] Response Headers:I0807 15:42:25.714751 13405 round_trippers.go:447] Audit-Id: a7dd4e83-e5d0-421b-b5b5-0f4de6fe3909I0807 15:42:25.714756 13405 round_trippers.go:447] Content-Type: application/jsonI0807 15:42:25.714761 13405 round_trippers.go:447] Date: Sun, 07 Aug 2022 07:42:25 GMTI0807 15:42:25.714845 13405 request.go:942] Response Body: {"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"apiregistration.k8s.io","versions":[{"groupVersion":"apiregistration.k8s.io/v1","version":"v1"},{"groupVersion":"apiregistration.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"apiregistration.k8s.io/v1","version":"v1"}},{"name":"extensions","versions":[{"groupVersion":"extensions/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"extensions/v1beta1","version":"v1beta1"}},{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"},{"groupVersion":"apps/v1beta2","version":"v1beta2"},{"groupVersion":"apps/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}},{"name":"events.k8s.io","versions":[{"groupVersion":"events.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"events.k8s.io/v1beta1","version":"v1beta1"}},{"name":"authentication.k8s.io","versions":[{"groupVersion":"authentication.k8s.io/v1","version":"v1"},{"groupVersion":"authentication.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"authentication.k8s.io/v1","version":"v1"}},{"name":"authorization.k8s.io","versions":[{"groupVersion":"authorization.k8s.io/v1","version":"v1"},{"groupVersion":"authorization.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"authorization.k8s.io/v1","version":"v1"}},{"name":"autoscaling","versions":[{"groupVersion":"autoscaling/v1","version":"v1"},{"groupVersion":"autoscaling/v2beta1","version":"v2beta1"},{"groupVersion":"autoscaling/v2beta2","version":"v2beta2"}],"preferredVersion":{"groupVersion":"autoscaling/v1","version":"v1"}},{"name":"batch","versions":[{"groupVersion":"batch/v1","version":"v1"},{"groupVersion":"batch/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"batch/v1","version":"v1"}},{"name":"certificates.k8s.io","versions":[{"groupVersion":"certificates.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"certificates.k8s.io/v1beta1","version":"v1beta1"}},{"name":"networking.k8s.io","versions":[{"groupVersion":"networking.k8s.io/v1","version":"v1"},{"groupVersion":"networking.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"networking.k8s.io/v1","version":"v1"}},{"name":"policy","versions":[{"groupVersion":"policy/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"policy/v1beta1","version":"v1beta1"}},{"name":"rbac.authorization.k8s.io","versions":[{"groupVersion":"rbac.authorization.k8s.io/v1","version":"v1"},{"groupVersion":"rbac.authorization.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"rbac.authorization.k8s.io/v1","version":"v1"}},{"name":"storage.k8s.io","versions":[{"groupVersion":"storage.k8s.io/v1","version":"v1"},{"groupVersion":"storage.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"storage.k8s.io/v1","version":"v1"}},{"name":"admissionregistration.k8s.io","versions":[{"groupVersion":"admissionregistration.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"admissionregistration.k8s.io/v1beta1","version":"v1beta1"}},{"name":"apiextensions.k8s.io","versions":[{"groupVersion":"apiextensions.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"apiextensions.k8s.io/v1beta1","version":"v1beta1"}},{"name":"scheduling.k8s.io","versions":[{"groupVersion":"scheduling.k8s.io/v1","version":"v1"},{"groupVersion":"scheduling.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"scheduling.k8s.io/v1","version":"v1"}},{"name":"coordination.k8s.io","versions":[{"groupVersion":"coordination.k8s.io/v1","version":"v1"},{"groupVersion":"coordination.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"coordination.k8s.io/v1","version":"v1"}},{"name":"node.k8s.io","versions":[{"groupVersion":"node.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"node.k8s.io/v1beta1","version":"v1beta1"}},{"name":"argoproj.io","versions":[{"groupVersion":"argoproj.io/v1alpha1","version":"v1alpha1"}],"preferredVersion":{"groupVersion":"argoproj.io/v1alpha1","version":"v1alpha1"}},{"name":"authentication.istio.io","versions":[{"groupVersion":"authentication.istio.io/v1alpha1","version":"v1alpha1"}],"preferredVersion":{"groupVersion":"authentication.istio.io/v1alpha1","version":"v1alpha1"}},{"name":"rbac.istio.io","versions":[{"groupVersion":"rbac.istio.io/v1alpha1","version":"v1alpha1"}],"preferredVersion":{"groupVersion":"rbac.istio.io/v1alpha1","version":"v1alpha1"}},{"name":"traefik.containo.us","versions":[{"groupVersion":"traefik.containo.us/v1alpha1","version":"v1alpha1"}],"preferredVersion":{"groupVersion":"traefik.containo.us/v1alpha1","version":"v1alpha1"}},{"name":"config.istio.io","versions":[{"groupVersion":"config.istio.io/v1alpha2","version":"v1alpha2"}],"preferredVersion":{"groupVersion":"config.istio.io/v1alpha2","version":"v1alpha2"}},{"name":"networking.istio.io","versions":[{"groupVersion":"networking.istio.io/v1alpha3","version":"v1alpha3"}],"preferredVersion":{"groupVersion":"networking.istio.io/v1alpha3","version":"v1alpha3"}},{"name":"security.istio.io","versions":[{"groupVersion":"security.istio.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"security.istio.io/v1beta1","version":"v1beta1"}},{"name":"metrics.k8s.io","versions":[{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}],"preferredVersion":{"groupVersion":"metrics.k8s.io/v1beta1","version":"v1beta1"}}]}I0807 15:42:25.715357 13405 round_trippers.go:419] curl -k -v -XGET -H "Accept: application/json, */*" -H "User-Agent: kubectl/v1.14.7 (linux/amd64) kubernetes/8fca2ec" 'https://kubemaster.cluster:6443/apis/metrics.k8s.io/v1beta1/nodes/k8smaster01-application.ali'I0807 15:42:25.734775 13405 round_trippers.go:438] GET https://kubemaster.cluster:6443/apis/metrics.k8s.io/v1beta1/nodes/k8smaster01-application.ali 200 OK in 19 millisecondsI0807 15:42:25.734797 13405 round_trippers.go:444] Response Headers:I0807 15:42:25.734802 13405 round_trippers.go:447] Audit-Id: 2a891333-1141-415b-995e-e2c5d02fe2feI0807 15:42:25.734807 13405 round_trippers.go:447] Content-Type: application/jsonI0807 15:42:25.734811 13405 round_trippers.go:447] Date: Sun, 07 Aug 2022 07:42:25 GMTI0807 15:42:25.734815 13405 round_trippers.go:447] Content-Length: 331I0807 15:42:25.734841 13405 request.go:942] Response Body: {"kind":"NodeMetrics","apiVersion":"metrics.k8s.io/v1beta1","metadata":{"name":"k8smaster01-application.ali","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k8smaster01-application.ali","creationTimestamp":"2022-08-07T07:42:25Z"},"timestamp":"2022-08-07T07:41:33Z","window":"30s","usage":{"cpu":"1102333605n","memory":"9202624Ki"}}I0807 15:42:25.748983 13405 round_trippers.go:419] curl -k -v -XGET -H "Accept: application/json, */*" -H "User-Agent: kubectl/v1.14.7 (linux/amd64) kubernetes/8fca2ec" 'https://kubemaster.cluster:6443/api/v1/nodes/k8smaster01-application.ali'I0807 15:42:25.997639 13405 round_trippers.go:438] GET https://kubemaster.cluster:6443/api/v1/nodes/k8smaster01-application.ali 200 OK in 248 millisecondsI0807 15:42:25.997662 13405 round_trippers.go:444] Response Headers:I0807 15:42:25.997667 13405 round_trippers.go:447] Audit-Id: 3af02c5e-69a6-4816-9359-dfabce9d6f9aI0807 15:42:25.997672 13405 round_trippers.go:447] Content-Type: application/jsonI0807 15:42:25.997677 13405 round_trippers.go:447] Date: Sun, 07 Aug 2022 07:42:25 GMTI0807 15:42:25.997759 13405 request.go:942] Response Body: {"kind":"Node","apiVersion":"v1","metadata":{"name":"k8smaster01-application.ali","selfLink":"/api/v1/nodes/k8smaster01-application.ali","uid":"2fa8762d-013c-11ea-81ab-00163e06bb08","resourceVersion":"1164983866","creationTimestamp":"2019-11-07T08:54:19Z","labels":{"beta.kubernetes.io/arch":"amd64","beta.kubernetes.io/os":"linux","k8s_cluster":"aliyun-bj-online-01","kubernetes.io/arch":"amd64","kubernetes.io/hostname":"k8smaster01-application.ali","kubernetes.io/os":"linux","node-role.kubernetes.io/master":""},"annotations":{"flannel.alpha.coreos.com/backend-data":"{\"VtepMAC\":\"22:c3:2f:2a:d8:bf\"}","flannel.alpha.coreos.com/backend-type":"vxlan","flannel.alpha.coreos.com/kube-subnet-manager":"true","flannel.alpha.coreos.com/public-ip":"172.26.25.137","kubeadm.alpha.kubernetes.io/cri-socket":"/var/run/dockershim.sock","node.alpha.kubernetes.io/ttl":"15","volumes.kubernetes.io/controller-managed-attach-detach":"true"}},"spec":{"podCIDR":"10.64.0.0/24","taints":[{"key":"node-role.kubernetes.io/master","effect":"NoSchedule"}]},"status":{"capacity":{"cpu":"4","ephemeral-storage":"103080204Ki","hugepages-1Gi":"0","hugepages-2Mi":"0","memory":"16155100Ki","pods":"110"},"allocatable":{"cpu":"4","ephemeral-storage":"94998715850","hugepages-1Gi":"0","hugepages-2Mi":"0","memory":"16052700Ki","pods":"110"},"conditions":[{"type":"MemoryPressure","status":"False","lastHeartbeatTime":"2022-08-07T07:41:38Z","lastTransitionTime":"2019-11-07T08:54:17Z","reason":"KubeletHasSufficientMemory","message":"kubelet has sufficient memory available"},{"type":"DiskPressure","status":"False","lastHeartbeatTime":"2022-08-07T07:41:38Z","lastTransitionTime":"2020-08-13T03:40:38Z","reason":"KubeletHasNoDiskPressure","message":"kubelet has no disk pressure"},{"type":"PIDPressure","status":"False","lastHeartbeatTime":"2022-08-07T07:41:38Z","lastTransitionTime":"2019-11-07T08:54:17Z","reason":"KubeletHasSufficientPID","message":"kubelet has sufficient PID available"},{"type":"Ready","status":"True","lastHeartbeatTime":"2022-08-07T07:41:38Z","lastTransitionTime":"2021-06-09T08:24:53Z","reason":"KubeletReady","message":"kubelet is posting ready status"}],"addresses":[{"type":"InternalIP","address":"172.26.25.137"},{"type":"Hostname","address":"k8smaster01-application.ali"}],"daemonEndpoints":{"kubeletEndpoint":{"Port":10250}},"nodeInfo":{"machineID":"f0f31005fb5a436d88e3c6cbf54e25aa","systemUUID":"10CB0DA3-8BCC-4245-9CF3-9062B4D3BEC8","bootID":"6621ee1d-534e-4c53-a44f-f32248abdf56","kernelVersion":"4.14.1-1.el7.elrepo.x86_64","osImage":"CentOS Linux 7 (Core)","containerRuntimeVersion":"docker://19.3.4","kubeletVersion":"v1.14.7","kubeProxyVersion":"v1.14.7","operatingSystem":"linux","architecture":"amd64"},"images":[{"names":["harbor.hualala.com/ci/baseservice/shop-base-service@sha256:9c7b5bcacfd76f89a7ad418e7cd25fe5ca50c10cc43d7ba5d227c2b190e6293f","registry2.hualala.com/shop-base-service@sha256:9c7b5bcacfd76f89a7ad418e7cd25fe5ca50c10cc43d7ba5d227c2b190e6293f","harbor.hualala.com/ci/baseservice/shop-base-service:bb5b969.235","harbor.hualala.com/ci/baseservice/shop-base-service:bb5b969.235sh","registry2.hualala.com/shop-base-service:bb5b969.235"],"sizeBytes":1085599691},{"names":["harbor.hualala.com/base/node@sha256:e56cbff0a409c97873fd0249adefa3bec7161e4bf486f01ff058b4734ab9b199","harbor.hualala.com/base/node:10.20.1"],"sizeBytes":911604404},{"names":["harbor.hualala.com/ci/baseservice/shop-base-service@sha256:591be1fe00f0b01b5a6aca845f46727071dee55f7b9ad72c0af3629a7dd718cc"],"sizeBytes":551545966},{"names":["harbor.hualala.com/base/node@sha256:06e9fd7a363e2f7d9253b91831d2d4f056429ab51551be1ba19ad78858e36907","harbor.hualala.com/base/node:16.13.2"],"sizeBytes":547886955},{"names":["harbor.hualala.com/base/node@sha256:0fa09c4055e707878f8459c1976cb056f2369462d317d2710abc0d64c91f9001","harbor.hualala.com/base/node:8.15.1"],"sizeBytes":514263667},{"names":["harbor.hualala.com/ci/filebeat/filebeat@sha256:a20270d0b4f07b083758f67bb39c775289fbb8f88c57257137f6e4216e0a7e12","harbor.hualala.com/ci/filebeat/filebeat:7.11.2"],"sizeBytes":464611372},{"names":["registry.cn-hangzhou.aliyuncs.com/google_containers/etcd@sha256:7872edc2929aa009ddcfeae6ff9a55b779890f46f5720fb3f01d3559c659db1b","registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.4.2"],"sizeBytes":281880100},{"names":["registry.aliyuncs.com/google_containers/kube-apiserver@sha256:a098084d3e65fe42acf886ae2a3c46a91cf60f74ea0d934b3cb9989b725faf3a","registry.aliyuncs.com/google_containers/kube-apiserver:v1.14.7"],"sizeBytes":209478462},{"names":["registry.aliyuncs.com/google_containers/kube-controller-manager@sha256:38c38df63ba9e765709a5d8737f59ac37ea56a311e7cd941c6215ce0667401f9","registry.aliyuncs.com/google_containers/kube-controller-manager:v1.14.7"],"sizeBytes":157503518},{"names":["harbor.hualala.com/ci/ocean/crab@sha256:565892d866cb737edef80c5abf984ba932241c23afe02013302a00fc757bbfc7","harbor.hualala.com/ci/ocean/crab:f1229da.66"],"sizeBytes":124654529},{"names":["registry.aliyuncs.com/google_containers/kube-proxy@sha256:6e09cc1d370b296cf19771f3112794cbf4bd59188f7cb4d37b3983d657b3bd2f","registry.aliyuncs.com/google_containers/kube-proxy:v1.14.7"],"sizeBytes":82106236},{"names":["registry.aliyuncs.com/google_containers/kube-scheduler@sha256:2e7dd61ef77805ea7af69697ca1a7727b1d5803c02e83f3463edb724029e120a","registry.aliyuncs.com/google_containers/kube-scheduler:v1.14.7"],"sizeBytes":81579742},{"names":["registry.cn-shanghai.aliyuncs.com/gcr-k8s/flannel@sha256:25e23320b5965ec8d5063ecf9f5a154372f6c230334dd11d76a0290184e789be","registry.cn-shanghai.aliyuncs.com/gcr-k8s/flannel:v0.10.0-amd64"],"sizeBytes":44598861},{"names":["registry.cn-beijing.aliyuncs.com/bj-aliyun/check@sha256:44ae2c9bc689e62d27c61e3484aa19731d993dc37ff4fb9a672c9d74632eb79a","registry.cn-beijing.aliyuncs.com/bj-aliyun/check:v1.0"],"sizeBytes":17997252},{"names":["registry2.hualala.com/apm@sha256:a0c2357e3aed55be6195187756ca5900ca570f0675b027bb198efd1604435bae","registry2.hualala.com/apm:052101"],"sizeBytes":14270047},{"names":["registry.aliyuncs.com/google_containers/pause@sha256:759c3f0f6493093a9043cc813092290af69029699ade0e3dbe024e968fcb7cca","registry.aliyuncs.com/google_containers/pause:3.1"],"sizeBytes":742472}]}}NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%k8smaster01-application.ali 1103m 27% 8986Mi 57%[root@k8smaster01-application ~]#
]]>
wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64chmod +x cfssl_linux-amd64 cfssljson_linux-amd64 cfssl-certinfo_linux-amd64cp cfssl_linux-amd64 /usr/local/bin/cfsslcp cfssljson_linux-amd64 /usr/local/bin/cfssljsoncp cfssl-certinfo_linux-amd64 /usr/bin/cfssl-certinfo # 生成ca证书,一般与kubernetes共用ca证书,不需要额外生成mkdir -p ~/TLS/{etcd,k8s}cd ~/TLS/etcdcat > /root/TLS/etcd/ca-config.json << EOF{ "signing": { "default": { "expiry": "87600h" }, "profiles": { "kubernetes": { "expiry": "87600h", "usages": [ "signing", "key encipherment", "server auth", "client auth" ] } } }}EOFcat > /root/TLS/etcd/ca-csr.json << EOF{ "CN": "kubernetes", "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "CN", "L": "Beijing", "ST": "Beijing", "O": "k8s", "OU": "system" } ]}EOF# 生成ca.pem和ca-key.pem文件cfssl gencert -initca ca-csr.json |cfssljson -bare ca -# 生成etcd证书cat > /root/TLS/etcd/server-csr.json << EOF{ "CN": "etcd", "hosts": [ "192.168.124.26", "192.168.124.27", "192.168.124.28", "192.168.124.29", "192.168.124.30", "192.168.124.31" ], "key": { "algo": "rsa", "size": 2048 }, "names": [ { "C": "CN", "L": "BeiJing", "ST": "BeiJing", "O": "k8s", "OU": "system" } ]}EOF# 生成证书文件cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes server-csr.json | cfssljson -bare server
ETCD_VER=v3.4.17DOWNLOAD_URL=https://github.com/etcd-io/etcd/releases/downloadmkdir -p /home/etcd/{data,ssl,config,bin}curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gztar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /home/etcd/bin --strip-components=1# 将ca-key.pem、ca.pem、server.pem、server-key.pem放到/home/etcd/ssl下# 创建etcd配置文件cat > /home/etcd/config/etcd.yml << EOFname: 'etcd-01'data-dir: '/home/etcd/data/default.etcd'# 本节点与其他节点进行数据交换(选举,数据同步)的监听地址listen-peer-urls: 'https://1.1.1.1:2380'# 监听地址,响应客户端请求listen-client-urls: 'https://1.1.1.1:2379'# 通知其他节点与本节点进行数据交换(选举,同步)的地址initial-advertise-peer-urls: 'https://1.1.1.1:2380'# 用于通知其他ETCD节点,客户端接入本节点的监听地址advertise-client-urls: 'https://1.1.1.1:2379'# 如果键空间的任何成员的后端数据库超过了空间配额,etcd发起集群范围的警告,让集群进入维护模式,仅接收键的读取和删除。quota-backend-bytes: 8589934592# 历史压缩,保持3小时key的历史记录auto-compaction-retention: "3"initial-cluster: 'etcd-01=https://1.1.1.1:2380,etcd-02=https://2.2.2.2:2380,etcd-03=https://3.3.3.3:2380'# 集群tokeninitial-cluster-token: 'etcd-demo-cluster'# 初始集群状态(new/existing),如果此选项设置为existing,etcd将尝试加入现有集群。initial-cluster-state: 'new'# 证书相关client-transport-security: cert-file: '/home/etcd/ssl/server.pem' key-file: '/home/etcd/ssl/server-key.pem' client-cert-auth: true trusted-ca-file: '/home/etcd/ssl/ca.pem'peer-transport-security: cert-file: '/home/etcd/ssl/server.pem' key-file: '/home/etcd/ssl/server-key.pem' client-cert-auth: true trusted-ca-file: '/home/etcd/ssl/ca.pem'# 日志格式logger: zapEOF# 创建system管理etcdcat > /usr/lib/systemd/system/etcd.service << EOF[Unit]Description=Etcd ServerAfter=network.targetAfter=network-online.targetWants=network-online.target[Service]Type=notifyExecStart=/home/etcd/bin/etcd --config-file=/home/etcd/config/etcd.ymlRestart=on-failureLimitNOFILE=65536[Install]WantedBy=multi-user.targetEOF# 启动并设置开机启动systemctl daemon-reloadsystemctl start etcd && systemctl status etcdsystemctl enable etcd
证书相关参数: etcdctl --endpoints=$ENDPOINTS --cert=$certPath --cacert=$cacertPat --key=$keyPath查看集群信息: etcdctl --endpoints=$ENDPOINTS endpoint status -wtable查看集群状态: etcdctl --endpoints=$ENDPOINTS endpoint health -wtable查看成员状态: etcdctl --endpoints=$ENDPOINTS member list -wtable查看告警信息: etcdctl --endpoints=$ENDPOINTS alarm listget: etcdctl --endpoints=$ENDPOINTS get /a etcdctl --endpoints=$ENDPOINTS get /a -wjson etcdctl --endpoints=$ENDPOINTS get --prefix / --keys-only etcdctl --endpoints=$ENDPOINTS get /a --rev=0 获取指定revision的值create: etcdctl --endpoints=$ENDPOINTS put foo "Hello World!"delete: etcdctl --endpoints=$ENDPOINTS del keywatch: etcdctl --endpoints=$ENDPOINTS watch stock1 etcdctl --endpoints=$ENDPOINTS watch stock --prefix# 备份etcdctl --endpoints=$ENDPOINTS snapshot save my.dbetcdctl --endpoints=$ENDPOINTS snapshot status my.db -wtable# 恢复etcdctl snapshot restore backup.db \ --name etcd-02 \ --data-dir=/home/etcd/data/default.etcd \ --initial-cluster "etcd-01=https://172.20.101.35:2380,etcd-02=https://172.20.141.201:2380,etcd-03=https://172.20.107.41:2380" \ --initial-cluster-token etcd-demo-cluster \ --initial-advertise-peer-urls https://172.20.141.201:2380member: etcdctl --endpoints=$ENDPOINTS member remove ${MEMBER_ID} etcdctl --endpoints=$ENDPOINTS member add ${ETCD_NAME} --peer-urls=http://${ETCD_IP}:2380 # Next, start the new member with --initial-cluster-state existing flag # 启动新节点时,指定 --initial-cluster-state 为existing
1. 获取节点IDetcdctl --endpoints=$ENDPOINTS --cert=$certPath --cacert=$cacertPat --key=$keyPath member list -wtable2. 踢出节点etcdctl --endpoints=$ENDPOINTS --cert=$certPath --cacert=$cacertPat --key=$keyPath member remove $memberID
1. 新部署etcd节点2. 添加节点etcdctl --endpoints=$ENDPOINTS --cert=$certPath --cacert=$cacertPat --key=$keyPath member add ${ETCD_NAME} --peer-urls=http://${ETCD_IP}:23803. 修改配置文件 initial-cluster-state 为 existing初始集群状态(new/existing),如果此选项设置为existing,etcd将尝试加入现有集群。4. 启动新节点systemctl start etcd
如果出现以下报错:
Jun 23 23:37:09 k8s-nacos-0001.dohko etcd[64335]: {"level":"warn","ts":"2022-06-23T23:37:09.475+0800","caller":"etcdserver/server.go:1095","msg":"server error","error":"the member has been permanently removed from the cluster"}Jun 23 23:37:09 k8s-nacos-0001.dohko etcd[64335]: {"level":"warn","ts":"2022-06-23T23:37:09.475+0800","caller":"etcdserver/server.go:1096","msg":"data-dir used by this member must be removed"}
证明该etcd节点还存在旧集群的元数据,无法新加到集群中。 需要将数据目录删除,然后重新启动。
etcd --name etcd-01 \ --initial-advertise-peer-urls http://192.168.244.13:2380 \--data-dir /home/etcd/data/default.etcd \--listen-peer-urls http://192.168.244.13:2380 \--listen-client-urls http://192.168.244.13:2379,http://127.0.0.1:2379 \--advertise-client-urls http://192.168.244.13:2379 \--initial-cluster-token etcd-cluster \--initial-cluster etcd-01=http://192.168.244.13:2380 \--initial-cluster-state=new \--force-new-cluster >> /tmp/etcd.log 2>&1 &
注意用到了 --force-new-cluster 参数,这个参数会重置集群ID和集群的所有成员信息。以单节点集群启动后,可以正常提供访问了。
1. 将故障节点的原有数据删除2. 添加节点etcdctl --endpoints=$ENDPOINTS --cert=$certPath --cacert=$cacertPat --key=$keyPath member add ${ETCD_NAME} --peer-urls=http://${ETCD_IP}:23803. 修改配置文件 initial-cluster-state 为 existing![etcd.png](https://blog.zs-fighting.cn/upload/2022/06/etcd-fb11fe708595405195a83efb9ea52e9d.png)初始集群状态(new/existing),如果此选项设置为existing,etcd将尝试加入现有集群。4. 启动新节点systemctl start etcd
Prometheus定义了4种不同的指标类型:Counter(计数器),Gauge(仪表盘),Histogram(直方图),Summary(摘要)。
这四类指标的特征为:
Counter
:只增不减(除非系统发生重启,或者用户进程有异常)的计数器。常见的监控指标如http_requests_total, node_cpu都是Counter类型的监控指标。一般推荐在定义为Counter的指标末尾加上_total作为后缀。
Gauge
:可增可减的仪表盘。Gauge类型的指标侧重于反应系统当前的状态。因此此类指标的数据可增可减。常见的例如node_memory_MemAvailable_bytes(可用内存)。
Histogram
:分析数据分布的直方图。显示数据的区间分布。例如统计请求耗时在0-10ms的请求数量和10ms-20ms的请求数量分布。
Summary
: 分析数据分布的摘要。显示数据的中位数,9分数等。
官方文档 WRITING EXPORTERS 介绍了编写 Exportor 的一些注意点。Prometheus 的 client 库提供了实现自定义 Exportor 的接口,Collector 接口定义了两个方法 Describe 和 Collect,实现这两个方法就可以暴露自定义的数据:
collector.go
package collectorimport ( "github.com/prometheus/client_golang/prometheus")//var ( FlowRegistry = prometheus.NewRegistry() //判断自定义collector是否实现了collector这个接口的所有方法 _ prometheus.Collector = (*FlowCollector)(nil))var testvalue = 0type FlowCollector struct { flowStatusDesc *prometheus.Desc}// 通过NewFlowCollector方法创建结构体及对应的指标信息func NewFlowCollector() *FlowCollector { return &FlowCollector{ // func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc flowStatusDesc: prometheus.NewDesc("azkaban_flows_status", "Azkaban flows status", []string{"project_id", "flow_id", "submit_user", "status"}, prometheus.Labels{"app": "azkaban"}, ), }}// 采集器必须实现prometheus.Collector接口,也必须实现Describe和Collect方法。func (a *FlowCollector) Describe(ch chan<- *prometheus.Desc) { ch <- a.flowStatusDesc}// go client Colletor只会在每次响应Prometheus请求的时候才收集数据// Collect方法是核心,它会抓取你需要的所有数据,根据需求对其进行分析,然后将指标发送回客户端库。// 用于传递所有可能指标的定义描述符 // 可以在程序运行期间添加新的描述,收集新的指标信息 func (a *FlowCollector) Collect(ch chan<- prometheus.Metric) { testvalue++ // func MustNewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) Metric ch <- prometheus.MustNewConstMetric(a.flowStatusDesc, prometheus.CounterValue, float64(testvalue), "projectId", "flowId", "submitUser", "failed", )}func init() { FlowRegistry.MustRegister(NewFlowCollector())}
main.go
package mainimport ( "azkaban_exporter/collector" "net/http" "log" "github.com/prometheus/client_golang/prometheus/promhttp")func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`<html> <head><title>azkaban_exporter</title></head> <body> <h1><a style="text-decoration:none" href=''>azkaban_exporter</a></h1> <p><a href='/metrics'>Metrics</a></p> <h2>Build</h2> <pre>v0.0.1</pre> </body> </html>`)) }) http.Handle("/metrics", promhttp.HandlerFor(collector.FlowRegistry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}), ) log.Fatal(http.ListenAndServe(":9101", nil))}
]]>
// containerd Service// 创建containerd clientfunc New(address string, opts ...ClientOpt) (*Client, error)// 拉取镜像,并创建Image对象func (c *Client) Pull(ctx context.Context, ref string, opts ...RemoteOpt) (_ Image, retErr error)// 创建containerfunc (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error)// 获取已存在container的元数据func (c *Client) LoadContainer(ctx context.Context, id string) (Container, error)------------------------------------------------------------------------------container Service// Container is a metadata object for container resources and task creationtype Container interface {// ID identifies the containerID() string// 返回底层容器记录类型Info(context.Context, ...InfoOpts) (containers.Container, error)// 删除容器Delete(context.Context, ...DeleteOpts) error// 基础容器元数据创建taskNewTask(context.Context, cio.Creator, ...NewTaskOpts) (Task, error)// 返回 OCI 运行时规范Spec(context.Context) (*oci.Spec, error)// 返回容器的当前任务// 如果传递了 cio.Attach 选项,客户端将重新连接到 IO 以进行运行任务// 客户端必须确保只有一个阅读器连接到任务并使用任务fifos的输出Task(context.Context, cio.Attach) (Task, error)// 返回容器所基于的镜像Image(context.Context) (Image, error)// 返回容器上设置的标签Labels(context.Context) (map[string]string, error)// 为容器设置提供的标签并返回最终的标签集SetLabels(context.Context, map[string]string) (map[string]string, error)// 返回容器上设置的扩展Extensions(context.Context) (map[string]prototypes.Any, error)// 更新容器Update(context.Context, ...UpdateContainerOpts) error// 创建当前容器的检查点镜像Checkpoint(context.Context, string, ...CheckpointOpts) (Image, error)}------------------------------------------------------------------------------namespaces Service// 设置namespacefunc WithNamespace(ctx context.Context, namespace string) context.Context------------------------------------------------------------------------------task Service// Task is the executable object within containerdtype Task interface {Process// 暂停任务的执行Pause(context.Context) error// 恢复任务的执行Resume(context.Context) error// 在任务中创建一个新进程Exec(context.Context, string, *specs.Process, cio.Creator) (Process, error)// 返回任务内系统特定进程 PID 的列表Pids(context.Context) ([]ProcessInfo, error)// 将任务的运行时和内存信息序列化成一个可以从远程资源推送和拉取的 OCI 索引Checkpoint(context.Context, ...CheckpointTaskOpts) (Image, error)// 使用更新的设置修改执行任务Update(context.Context, ...UpdateTaskOpts) error// 加载先前创建的执行进程LoadProcess(context.Context, string, cio.Attach) (Process, error)// 返回运行时特定指标的任务指标Metrics(context.Context) (*types.Metric, error)// 返回任务的当前 OCI 规范Spec(context.Context) (*oci.Spec, error)}
可以做成K8S登陆终端
package mainimport ("context""fmt""io""log""syscall""github.com/containerd/containerd""github.com/containerd/containerd/cio""github.com/containerd/containerd/namespaces""github.com/containerd/containerd/oci""github.com/google/uuid""github.com/opencontainers/runtime-spec/specs-go")var (_dockerNamespace = "moby"_containerdNamespace = "k8s.io"_image = "docker.io/library/busybox:latest"_containerdEndpoint = "/run/containerd/containerd.sock"_containerId = "9d238888faff3ebf3e55f33cd98848902c7594a9fc6aab4f62bfbd9e5c8929b6"_defaultCommand = []string{"sh", "-l"}_containerUuid = fmt.Sprintf("container-demo-" + uuid.New().String()))func main() {ctx := namespaces.WithNamespace(context.Background(), _dockerNamespace)stdin, stdout := io.Pipe()// 1、获取正在运行container的pid以及mount信息client, _ := containerd.New(_containerdEndpoint)container, _ := client.LoadContainer(ctx, _containerId)task, _ := container.Task(ctx, nil)pids, _ := task.Pids(ctx)pid := int64(pids[0].Pid)log.Printf("get containerID: %s,pid: %d\n", _containerId, pid)image, _ := client.Pull(ctx, _image, containerd.WithPullUnpack)// 2、生成默认的OCI标准var ops []oci.SpecOpts// 生成 oci 标准的 image 回调函数ops = append(ops, oci.WithImageConfig(image))// 生成 oci 标准的 特权 回调函数ops = append(ops, oci.WithPrivileged)// 生成 oci 标准 tty 回调函数ops = append(ops, oci.WithTTY)// 生成 oci 标准的 command 回调函数ops = append(ops, oci.WithProcessArgs(_defaultCommand...))// 生成 oci 标准的 数据卷挂载 结构 回调函数//ops = append(ops, nil)// 3、把正在运行container的Namespace加到OCI标准中ops = append(ops, oci.WithLinuxNamespace(specs.LinuxNamespace{Type: specs.UTSNamespace,Path: fmt.Sprintf("/proc/%v/ns/uts", pid),}))ops = append(ops, oci.WithLinuxNamespace(specs.LinuxNamespace{Type: specs.NetworkNamespace,Path: fmt.Sprintf("/proc/%v/ns/net", pid),}))ops = append(ops, oci.WithLinuxNamespace(specs.LinuxNamespace{Type: specs.PIDNamespace,Path: fmt.Sprintf("/proc/%v/ns/pid", pid),}))//ops = append(ops, oci.WithLinuxNamespace(specs.LinuxNamespace{//Type: specs.CgroupNamespace,//Path: fmt.Sprintf("/proc/%v/ns/uts", pid),//}))//ops = append(ops, oci.WithLinuxNamespace(specs.LinuxNamespace{//Type: specs.UserNamespace,//Path: fmt.Sprintf("/proc/%v/ns/user", pid),//}))//ops = append(ops, oci.WithLinuxNamespace(specs.LinuxNamespace{//Type: specs.MountNamespace,//Path: fmt.Sprintf("/proc/%v/ns/mnt", pid),//}))// 4、根据OCI标准生成container元数据,运行task// 初始化 containernewContainer, _ := client.NewContainer(ctx,_containerUuid,containerd.WithNewSnapshot(_containerUuid, image),containerd.WithNewSpec(ops...))defer func() {newContainer.Delete(ctx, containerd.WithSnapshotCleanup)}()log.Printf("new container is successful %s", newContainer.ID())// 设置 cio 标准的 streamsstreamIO := cio.WithStreams(stdin, stdout, stdout)// 初始化 containerd tasknewTask, err := newContainer.NewTask(ctx, cio.NewCreator(streamIO, cio.WithTerminal))defer func() {newTask.Kill(ctx, syscall.SIGTERM|syscall.SIGKILL)newTask.Delete(ctx, containerd.WithProcessKill)}()if newTask == nil || err != nil {log.Printf("new container task is failed %s: %v\n", _containerId, err)}log.Printf("new container task is successful %s", newTask.ID())exitStatusCh, _ := newTask.Wait(ctx)newTask.Start(ctx)status := <-exitStatusChcode, _, _ := status.Result()log.Printf("task exited with status %d\n", code)}
NAME: ctr - __ _____/ /______ / ___/ __/ ___// /__/ /_/ /\___/\__/_/containerd CLIUSAGE: ctr [global options] command [command options] [arguments...]VERSION: 1.4.12DESCRIPTION: ctr is an unsupported debug and administrative client for interactingwith the containerd daemon. Because it is unsupported, the commands,options, and operations are not guaranteed to be backward compatible orstable from release to release of the containerd project.COMMANDS: plugins, plugin provides information about containerd plugins version print the client and server versions containers, c, container manage containers content manage content events, event display containerd events images, image, i manage images leases manage leases namespaces, namespace, ns manage namespaces pprof provide golang pprof outputs for containerd run run a container snapshots, snapshot manage snapshots tasks, t, task manage tasks install install a new package oci OCI tools shim interact with a shim directly help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --debug enable debug output in logs --address value, -a value address for containerd's GRPC server (default: "/run/containerd/containerd.sock") [$CONTAINERD_ADDRESS] --timeout value total timeout for ctr commands (default: 0s) --connect-timeout value timeout for connecting to containerd (default: 0s) --namespace value, -n value namespace to use with commands (default: "default") [$CONTAINERD_NAMESPACE] --help, -h show help --version, -v print the version
ctr image lsctr image pull docker.io/library/nginx:alpinectr image tag docker.io/library/nginx:alpine harbor.k8s.local/course/nginx:alpinectr image rm harbor.k8s.local/course/nginx:alpine// 将镜像挂载到主机目录ctr image mount docker.io/library/nginx:alpine /mnt// 将镜像从主机目录上卸载ctr image unmount /mnt// 导出ctr image export nginx.tar.gz docker.io/library/nginx:alpine// 导入ctr image import nginx.tar.gz
ctr container ls// 类似于 docker inspect 功能ctr container info nginxctr container rm nginx
上面我们通过 container create 命令创建的容器,并没有处于运行状态,只是一个静态的容器。一个 container 对象只是包含了运行一个容器所需的资源及相关配置数据,表示 namespaces、rootfs 和容器的配置都已经初始化成功了,只是用户进程还没有启动。
一个容器真正运行起来是由 Task 任务实现的,Task 可以为容器设置网卡,还可以配置工具来对容器进行监控等。
ctr task ls// 使用 exec 命令进入容器进行操作ctr task exec --exec-id 0 -t nginx sh// 暂停容器ctr task pause nginx// 恢复容器ctr task resume nginxctr task kill nginxctr task rm nginxctr task metrics nginx
]]>
采用管道的通信方式,在Go程序中运行 子进程
并实现通信。✉️
按照通用的规则,可以解耦程序,实现插件化。
字节开源的安全项目 Elkeid
就是使用的这种方式实现插件化。
https://github.com/bytedance/Elkeid/blob/main/agent/plugin/plugin_linux.go#L43
进程间的通信方式参考下面文章:
进程间通信IPC (InterProcess Communication)
pstree
➜ testProject pstree 29475 -+= 29475 zhangshun ./server \--- 29476 zhangshun /Users/zhangshun/hll/gitlab/testProject/test/pipe/client/plugin
parent
func main() {workDir := "/Users/zhangshun/hll/gitlab/testProject/test/pipe/client"execPath := path.Join(workDir, "plugin")cmd := exec.Command(execPath)// 创建两个管道,实现全双工parentReader, childWriter, err := os.Pipe()if err != nil {panic(err)}childReader, parentWriter, err := os.Pipe()if err != nil {panic(err)}// 创建子进程的标准错误文件errFile, err := os.OpenFile(execPath+".stderr", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0o644)defer errFile.Close()if err != nil {panic(err)}cmd.Dir = workDircmd.Stderr = errFile // 将父进程打开的文件传给子进程 // 除了标准输入输出0,1,2三个文件外,还可以将父进程的文件传给子进程cmd.ExtraFiles = append(cmd.ExtraFiles, childReader, childWriter)cmd.Start()wg := &sync.WaitGroup{}wg.Add(3)// 等待子进程结束go func() {defer wg.Done()cmd.Wait()parentReader.Close()parentWriter.Close()}()// 读取子进程go func() {defer wg.Done()reader := bufio.NewReader(parentReader)buf := make([]byte, 1024)for {n, err := reader.Read(buf)if err != nil {fmt.Println(err)}fmt.Print(string(buf[:n]))}}()// 写入子进程go func() {defer wg.Done()writer := bufio.NewWriterSize(parentWriter, 1024*256)for p := 0; p < 10; p++ {content := []byte(fmt.Sprintf("parent write data: %d\n", p))_, err := writer.Write(content)if err != nil {fmt.Println(err)}err = writer.Flush()if err != nil {fmt.Println(err)}time.Sleep(time.Second * 1)}}()wg.Wait()}
child
func main() { // 实例化父进程传过来的fd // 除了标准输入输出0,1,2三个文件外,还可以将父进程的文件传给子进程reader := bufio.NewReaderSize(os.NewFile(3, "pipe"), 1024*128)writer := bufio.NewWriterSize(os.NewFile(4, "pipe"), 1024*128)// 把父进程的输入写到文件中logFile, err := os.OpenFile("plugin.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0o644)defer logFile.Close()if err != nil {panic(err)}logWriter := bufio.NewWriterSize(logFile, 2)wg := &sync.WaitGroup{}wg.Add(2)// 读取父进程go func() {defer wg.Done()buf := make([]byte, 1024)for {n, err := reader.Read(buf)if err != nil && err != io.EOF {break}logWriter.Write(buf[:n])logWriter.Flush()}}()// 写入父进程go func() {defer wg.Done()for i := 0; i < 10; i++ {content := []byte(fmt.Sprintf("child write data: %d\n", i))writer.Write(content)writer.Flush()time.Sleep(time.Second * 1)}}()wg.Wait()}
type Cmd struct { Path string // 运行命令的路径,绝对路径或者相对路径 Args []string // 命令参数 Env []string // 进程环境,如果环境为空,则使用当前进程的环境 Dir string // 指定command的工作目录,如果dir为空,则comman在调用进程所在当前目录中运行 Stdin io.Reader // 标准输入,如果stdin是nil的话,进程从null device中读取(os.DevNull),stdin也可以时一个 // 文件,否则的话则在运行过程中再开一个goroutine去/读取标准输入 Stdout io.Writer // 标准输出 Stderr io.Writer // 错误输出,如果这两个(Stdout和Stderr)为空的话,则command运行时将响应的文件描述符连接到 // os.DevNull ExtraFiles []*os.File // 除了标准输入输出0,1,2三个文件外,还可以将父进程的文件传给子进程,打开的文件描述符切片,可为进程添加fd,比如 socket SysProcAttr *syscall.SysProcAttr // 系统的进程属性 Process *os.Process // Process是底层进程,只启动一次,就是 os.StartProcess 返回的进程对象 ProcessState *os.ProcessState // ProcessState包含一个退出进程的信息,当进程调用Wait或者Run时便会产生该信息. }
]]>
// os.File// 文件操作// func Open(name string) (file *File, err error)// Open打开一个文件用于读取。O_RDONLY模式。// func OpenFile(name string, flag int, perm FileMode) (file *File, err error)// OpenFile是文件打开函数。指定打开模式。const ( O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件 O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件 O_RDWR int = syscall.O_RDWR // 读写模式打开文件 O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部 O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件 O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在 O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件)// func NewFile(fd uintptr, name string) *File// NewFile使用给出的Unix文件描述符和名称创建一个文件。// func Pipe() (r *File, w *File, err error)// Pipe返回一对关联的文件对象。从r的读取将返回写入w的数据。// io// 属于底层接口定义库,其作用是是定义一些基本接口和一些基本常量。// 常见的接口有Reader、Writer等。一般用这个库只是为了调用它的一些常量,比如io.EOF。// bufio// io库上再封装一层,加上了缓存功能。// 读写缓存,减少io操作的次数。// func NewReader(rd io.Reader) *Reader// 使用默认缓冲区大小。defaultBufSize = 4096// func NewReaderSize(rd io.Reader, size int) *Reader// 使用自定义缓冲区大小// func NewWriter(w io.Writer) *Writer// 使用默认缓冲区大小。defaultBufSize = 4096// func NewWriterSize(w io.Writer, size int) *Writer// 使用自定义缓冲区大小// func (b *Reader) Read(p []byte) (n int, err error)// Read 从 b 中读出数据到 p 中,返回读出的字节数和遇到的错误。// 如果缓存不为空,则只能读出缓存中的数据,不会从底层 io.Reader中提取数据。// 如果缓存为空,则:// 1、len(p) >= 缓存大小,则跳过缓存,直接从底层 io.Reader 中读出到 p 中。// 2、len(p) < 缓存大小,则先将数据从底层 io.Reader 中读取到缓存中,再从缓存读取到 p 中。// func (b *Reader) ReadString(delim byte) (string, error)// func (b *Reader) ReadBytes(delim byte) ([]byte, error)// func (b *Writer) Write(p []byte) (nn int, err error)// 当写入内容小于缓冲区(buf)的可用大小时,内容写入缓存区(buf);// 当缓冲区(buf)空间不够时,一次性将缓冲区(buf)内容写入文件,并清空缓存区(buf);// 当写入内容大于缓冲区(buf)空间时,将内容直接写入文件;// func (b *Writer) WriteByte(c byte) error// func (b *Writer) WriteString(s string) (int, error)// func (b *Writer) Flush() error// ioutil// 主要作用是作为一个工具包,里面有一些比较实用的函数。// 唯一需要注意的是它们都是一次性读取和一次性写入,所以当读取的时候注意文件不能过大。// func ReadAll(r io.Reader) ([]byte, error)// func ReadFile(filename string) ([]byte, error)// func WriteFile(filename string, data []byte, perm fs.FileMode) error
1. 全部读取
func readAll1() { file, err := os.Open("a.txt") if err != nil { panic(err) } defer file.Close() content, err := ioutil.ReadAll(file) fmt.Println(string(content))}func readAll2() { content ,err :=ioutil.ReadFile("a.txt") if err !=nil { panic(err) } fmt.Println(string(content))}
2. 按字节读取文件
func readByte() { file, err := os.Open("a.txt") if err != nil { panic(err) } defer file.Close() // 使用默认缓存,defaultBufSize = 4096 reader := bufio.NewReader(file) // 使用自定义缓存 reader := bufio.NewReaderSize(file, 1024*128) chunks := make([]byte, 0) buf := make([]byte, 1024) for { n, err := reader.Read(buf) // 读取报错 if err != nil && err != io.EOF { panic(err) } fmt.Println(string(buf[:n])) // 读取完毕 if 0 == n || err == io.EOF { break } chunks = append(chunks, buf[:n]...) } fmt.Println(string(chunks))}
3. 按行读取
func readLine() { file, err := os.Open("a.txt") if err != nil { fmt.Println("Open file error!", err) return } defer file.Close() // 使用默认缓存,defaultBufSize = 4096 reader := bufio.NewReader(file) // 使用自定义缓存 reader := bufio.NewReaderSize(file, 1024*128) for { line, err := reader.ReadString('\n') // 读取报错 if err != nil && err != io.EOF { panic(err) } fmt.Println(line) // 读取完毕 if err == io.EOF { fmt.Println("read finished") break } }}
1. ioutil.WriteFile
func writeByIoutil() { content := []byte("测试1\n测试2\n") err := ioutil.WriteFile("test.txt", content, 0644) if err != nil { panic(err) }}
这种方式每次都会覆盖 test.txt内容,如果test.txt文件不存在会创建。
2. bufio
func writeByBufio() { file, err := os.OpenFile("a.txt",os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644) if err != nil { fmt.Println("Open file error!", err) return } defer file.Close() writer := bufio.NewWriterSize(file, 1024*128) content := []byte(time.Now().String() + "\n") n, err := writer.Write(content) if err != nil { fmt.Println(err) } fmt.Printf("写入 %d 个字节n", n) // 将缓存中的所有数据写入底层的 io.Writer 对象中 // 不主动刷新的话,缓存满了也会刷新到 io.Writer 对象中 writer.Flush()}
<target> : <prerequisites>[tab]<commands>
@
: 禁止回声,终端不会打印真实的执行命令#
: 表示注释${val}
: 表示变量$ make helpUsage: make <TARGETS> <OPTIONS> ...Targets: # 代码生成类命令 gen Generate all necessary files, such as error code files. # 格式化类命令 format Gofmt (reformat) package sources (exclude vendor dir if existed). # 静态代码检查 lint Check syntax and styling of go sources. # 测试类命令 test Run unit test. cover Run unit test and get test coverage. # 构建类命令 build Build source code for host platform. build.multiarch Build source code for multiple platforms. See option PLATFORMS. # Docker镜像打包类命令 image Build docker images for host arch. image.multiarch Build docker images for multiple platforms. See option PLATFORMS. push Build docker images for host arch and push images to registry. push.multiarch Build docker images for multiple platforms and push images to registry. # 部署类命令 deploy Deploy updated components to development env. # 清理类命令 clean Remove all files that are created by building. # 其他命令,不同项目会有区别 release Release iam verify-copyright Verify the boilerplate headers for all files. ca Generate CA files for all iam components. install Install iam system with all its components. swagger Generate swagger document. tools install dependent tools. # 帮助命令 help Show this help info.
## help: Show this help info..PHONY: helphelp: Makefile@echo -e "\nUsage: make <TARGETS> ...\n\nTargets:"@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
]]>
type Foo struct { name string id int age int db interface{}}// FooOption 代表可选参数type FooOption func(foo *Foo)// WithName 代表Name为可选参数func WithName(name string) FooOption { return func(foo *Foo) { foo.name = name }}// WithAge 代表age为可选参数func WithAge(age int) FooOption { return func(foo *Foo) { foo.age = age }}// WithDB 代表db为可选参数func WithDB(db interface{}) FooOption { return func(foo *Foo) { foo.db = db }}// NewFoo 代表初始化func NewFoo(id int, options ...FooOption) *Foo { foo := &Foo{ name: "default", id: id, age: 10, db: nil, } for _, option := range options { option(foo) } return foo}
这样以后,我们初始化结构体时,就变成了这个样子:
// 具体使用NewFoo的函数func Bar() { foo := NewFoo(1, WithAge(15), WithName("foo")) fmt.Println(foo)}
如果后续Foo增加属性,那么我们只需要增加WithXXX的方法就可以了,而NewFoo函数不需要任何变化,扩展性非常好。
这种Option的写法,在很多著名的库中都有用到,比如:gorm、go-redis等,如果需要对一个比较复杂的结构体初始化的时候,这种方法应该是最优的方式了。
]]>Fig 是一款Mac终端的自动补全工具,通过实时提示和补全终端命令,和自动的补全快捷键不同,Fig 不仅是提供了更多命令支持,更重要的是它带来了可交互的界面,可以让你更轻松地使用命令行
在第一次开启Fig时,它会有一个欢迎的界面,提供了一些简单的使用教程,你可以按 Enter 键查看教程,也可以按 Ctrl + C 跳过,不过第一次使用还是建议看完教程
当你在终端中输入命令后,会看到实时的提示,把可执行的命令以菜单的形式展示出来,你可以使用键盘选择想要输入的命令,也可以直接用鼠标点击,而且支持连续提示
Fig 还可以自动补全和提示参数,在运行一些不熟悉的命令时,可以直接用它来查看可用的参数,还会给你相应的说明,不用再去查看文档
Fig 支持多种命令,系统文件操作、Git、NPM、Docker、SSH、Heroku库等,用途很广泛
Fig 还有图形化的设置界面、调试模式等,可以选择自己的偏好设置,对于开发者非常友好
另外,Fig 还在持续增加功能,以后还会有图形化的可操作界面
Fig 是一款非常好用的终端增强工具,如果你的工作中经常用到终端命令,赶紧尝试一下吧
]]>Operator 是 Kubernetes 的扩展软件,它利用 定制资源管理应用及其组件。 Operator 遵循 Kubernetes 的理念,特别是在控制器方面。
通过 Operator 的方案,可以对 Kubernetes 的功能进行友好地扩展。Operatpr = CRD + Controller
。首先通过 yaml 定义,生成 CRD ,然后 Controller 不断地监听 etcd 中的数据,执行相应动作。开发 Operator 时,有很多繁琐且重复的事情。KubeBuilder 可以帮助我们快速生成骨架代码,开发一个 Kubernetes 的扩展功能。
# 初始化项目kubebuilder init --domain zhangshun.io --license apache2 --owner zhangshun# 创建APIkubebuilder create api --group apps --version v1beta1 --kind MyWeb# 部署CRD到k8smake install# 本地运行make run# 构建镜像并上传至仓库make docker-build docker-push IMG=zhangshunzz/myweb:v0.1# 部署controller到k8smake deploy IMG=zhangshunzz/myweb:v0.1# 从集群中删除CRDmake uninstall# 从集群中卸载控制器make undeploy
定义CRD
type MyWebSpec struct {// 业务服务对应的镜像,包括名称:tagImage string `json:"image"`// service占用的宿主机端口,外部请求通过此端口访问pod的服务Port *int32 `json:"port"`// 单个pod的QPS上限SinglePodQPS *int32 `json:"singlePodQPS"`// 当前整个业务的总QPSTotalQPS *int32 `json:"totalQPS"`// 资源限制Resources v1.ResourceRequirements `json:"resources"`}type MyWebStatus struct {// 当前kubernetes中实际支持的总QPSRealQPS *int32 `json:"realQPS"`}
方法getExpectReplicas
/ 根据单个QPS和总QPS计算pod数量func getExpectReplicas(myWeb *appsv1beta1.MyWeb) int32 {// 单个pod的QPSsinglePodQPS := *(myWeb.Spec.SinglePodQPS)// 期望的总QPStotalQPS := *(myWeb.Spec.TotalQPS)// Replicas就是要创建的副本数replicas := totalQPS / singlePodQPSif totalQPS%singlePodQPS > 0 {replicas++}return replicas}
方法createServiceIfNotExists
// 新建servicefunc createServiceIfNotExists(ctx context.Context, r *MyWebReconciler, myWeb *appsv1beta1.MyWeb, req ctrl.Request) error {log := r.Log.WithValues("func", "createService")service := &corev1.Service{}err := r.Get(ctx, req.NamespacedName, service)// 如果查询结果没有错误,证明service正常,就不做任何操作if err == nil {log.Info("service exists")return nil}// 如果错误不是NotFound,就返回错误if !errors.IsNotFound(err) {log.Error(err, "query service error")return err}// 实例化一个数据结构service = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: myWeb.Namespace,Name: myWeb.Name,},Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{Name: "http",Port: 8080,NodePort: *myWeb.Spec.Port,},},Selector: map[string]string{"app": APP_NAME,},Type: corev1.ServiceTypeNodePort,},}// 这一步非常关键!// 建立关联后,删除elasticweb资源时就会将deployment也删除掉log.Info("set reference")if err := controllerutil.SetControllerReference(myWeb, service, r.Scheme); err != nil {log.Error(err, "SetControllerReference error")return err}// 创建servicelog.Info("start create service")if err := r.Create(ctx, service); err != nil {log.Error(err, "create service error")return err}log.Info("create service success")return nil}
方法createDeployment
// 新建deploymentfunc createDeployment(ctx context.Context, r *MyWebReconciler, myWeb *appsv1beta1.MyWeb) error {log := r.Log.WithValues("func", "createDeployment")// 计算期望的pod数量expectReplicas := getExpectReplicas(myWeb)log.Info(fmt.Sprintf("expectReplicas [%d]", expectReplicas))// 实例化一个数据结构deployment := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: myWeb.Namespace,Name: myWeb.Name,},Spec: appsv1.DeploymentSpec{// 副本数是计算出来的Replicas: pointer.Int32Ptr(expectReplicas),Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": APP_NAME,},},Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": APP_NAME,},},Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: APP_NAME,// 用指定的镜像Image: myWeb.Spec.Image,ImagePullPolicy: "IfNotPresent",Ports: []corev1.ContainerPort{{Name: "http",Protocol: corev1.ProtocolSCTP,ContainerPort: CONTAINER_PORT,},},Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{"cpu": resource.MustParse(CPU_REQUEST),"memory": resource.MustParse(MEM_REQUEST),},Limits: corev1.ResourceList{"cpu": resource.MustParse(CPU_LIMIT),"memory": resource.MustParse(MEM_LIMIT),},},},},},},},}// 这一步非常关键!// 建立关联后,删除elasticweb资源时就会将deployment也删除掉log.Info("set reference")if err := controllerutil.SetControllerReference(myWeb, deployment, r.Scheme); err != nil {log.Error(err, "SetControllerReference error")return err}// 创建deploymentlog.Info("start create deployment")if err := r.Create(ctx, deployment); err != nil {log.Error(err, "create deployment error")return err}log.Info("create deployment success")return nil}
方法updateStatus
// 完成了pod的处理后,更新最新状态func updateStatus(ctx context.Context, r *MyWebReconciler, myWeb *appsv1beta1.MyWeb) error {log := r.Log.WithValues("func", "updateStatus")// 单个pod的QPSsinglePodQPS := *(myWeb.Spec.SinglePodQPS)// pod总数replicas := getExpectReplicas(elasticWeb)// 当pod创建完毕后,当前系统实际的QPS:单个pod的QPS * pod总数// 如果该字段还没有初始化,就先做初始化if nil == myWeb.Status.RealQPS {myWeb.Status.RealQPS = new(int32)}*(myWeb.Status.RealQPS) = singlePodQPS * replicaslog.Info(fmt.Sprintf("singlePodQPS [%d], replicas [%d], realQPS[%d]", singlePodQPS, replicas, *(elasticWeb.Status.RealQPS)))if err := r.Update(ctx, myWeb); err != nil {log.Error(err, "update instance error")return err}return nil}
Reconcile主干代码
func (r *MyWebReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {// 会用到contextctx := context.Background()log := r.Log.WithValues("myweb", req.NamespacedName)// your logic herelog.Info("1. start reconcile logic")// 实例化数据结构instance := &appsv1beta1.MyWeb{}// 通过客户端工具查询,查询条件是err := r.Get(ctx, req.NamespacedName, instance)if err != nil {// 如果没有实例,就返回空结果,这样外部就不再立即调用Reconcile方法了if errors.IsNotFound(err) {log.Info("2.1. instance not found, maybe removed")return reconcile.Result{}, nil}log.Error(err, "2.2 error")// 返回错误信息给外部return ctrl.Result{}, err}log.Info("3. instance : " + instance.String())// 查找deploymentdeployment := &appsv1.Deployment{}// 用客户端工具查询err = r.Get(ctx, req.NamespacedName, deployment)// 查找时发生异常,以及查出来没有结果的处理逻辑if err != nil {// 如果没有实例就要创建了if errors.IsNotFound(err) {log.Info("4. deployment not exists")// 如果对QPS没有需求,此时又没有deployment,就啥事都不做了if *(instance.Spec.TotalQPS) < 1 {log.Info("5.1 not need deployment")// 返回return ctrl.Result{}, nil}// 先要创建serviceif err = createServiceIfNotExists(ctx, r, instance, req); err != nil {log.Error(err, "5.2 error")// 返回错误信息给外部return ctrl.Result{}, err}// 立即创建deploymentif err = createDeployment(ctx, r, instance); err != nil {log.Error(err, "5.3 error")// 返回错误信息给外部return ctrl.Result{}, err}// 如果创建成功就更新状态if err = updateStatus(ctx, r, instance); err != nil {log.Error(err, "5.4. error")// 返回错误信息给外部return ctrl.Result{}, err}// 创建成功就可以返回了return ctrl.Result{}, nil} else {log.Error(err, "7. error")// 返回错误信息给外部return ctrl.Result{}, err}}// 如果查到了deployment,并且没有返回错误,就走下面的逻辑// 根据单QPS和总QPS计算期望的副本数expectReplicas := getExpectReplicas(instance)// 当前deployment的期望副本数realReplicas := *deployment.Spec.Replicaslog.Info(fmt.Sprintf("9. expectReplicas [%d], realReplicas [%d]", expectReplicas, realReplicas))// 如果相等,就直接返回了if expectReplicas == realReplicas {log.Info("10. return now")return ctrl.Result{}, nil}// 如果不等,就要调整*(deployment.Spec.Replicas) = expectReplicaslog.Info("11. update deployment's Replicas")// 通过客户端更新deploymentif err = r.Update(ctx, deployment); err != nil {log.Error(err, "12. update deployment replicas error")// 返回错误信息给外部return ctrl.Result{}, err}log.Info("13. update status")// 如果更新deployment的Replicas成功,就更新状态if err = updateStatus(ctx, r, instance); err != nil {log.Error(err, "14. update status error")// 返回错误信息给外部return ctrl.Result{}, err}return ctrl.Result{}, nil}
Kubelet 进程会限制最大 Pod数,因为 Kubelet会每秒 gRPC调用 CRI查询 Pod信息(PLEG部分),并上报到 Kube-ApiServer。
如果 Pod数量很多,可能会导致 CRI接口超时或崩溃。
CRI 大体包含三部分接口:Sandbox 、 Container 和 Image。
https://github.com/kubernetes/cri-api/blob/c75ef5b/pkg/apis/runtime/v1/api.proto
Sandbox 是Pod 创建时最先启动的 Container,为 Container 提供一定的运行环境,这其中包括 pod 的网络等。
PodSandbox 其实就是 pause 容器。
CRI 是 Kubernetes定义的一组 gRPC服务。Kubelet 作为客户端,基于 gRPC框架,通过 Socket 和容器运行时通信。
它包括两类服务:镜像服务(Image Service)和运行时服务(Runtime Service)。
镜像服务:提供下载、检查和删除镜像的远程程序调用。
运行时服务:包含用于管理容器生命周期,以及与容器交互的调用(exec/attach/port-forward)的远程程序调用。
Docker-shim、Containerd 和 CRI-O都是遵循 CRI的容器运行时,称为高层级运行时。
OCI(Open Container Initiative,开放容器计划)定义了创建容器的格式和运行时的开源行业标准,包括 镜像规范 和 运行时规范。
镜像规范定义了 OCI镜像的标准。高层级运行时将会下载一个 OCI镜像,并把它解压成 OCI运行时文件系统包。
运行时规范则描述了如何从 OCI运行时文件系统包运行容器程序。如何为新容器设置 Namespace 和 Cgroup。它的一个参考实现是 runC。称为低层级运行时
在Kubernetes中,提供了一个轻量的通用容器网络接口CNI,专门用于设置和删除容器的网络联通性。
容器运行时通过 CNI调用网络插件来完成容器的网络设置。
Kubelet 来查找 CNI插件的,运行插件来为容器设置网络,这两个参数应该配置在Kubelet处:
cni-cin-dir: 网络插件的可执行文件所在目录,默认是/opt/cni/bin
cni-conf-dir: 网络插件的配置文件所在目录,默认是/etc/cni/net.d
VXLAN 模式是通过 UDP协议进行封包,需要 CNI插件在用户态封包、解包,效率比较低。
VXLAN模式
数据包首先会通过 veth pair
达到 Node节点,然后都会经过 4789端口对应的进程,进行封包、解包。
IPIP模式
从字面上理解,就是把一个IP数据包又套在一个IP包里,即把IP层封装到IP层的一个Tunnel。它的作用其实基本上就相当于一个基于IP层的网桥。一般来说,普通的网桥是基于MAC层的,不需要IP,而这个IP则是通过两端的路由做一个Tunnel,把两个本来不通的网络通过点对点连接起来。
BGP模式
Calico 项目实际上将集群里的所有节点,都当作是边界路由器来处理,它们一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则
这里最核心的 下一跳 路由规则,就是由 Calico 的 Felix 进程负责维护的。这些路由规则信息,则是通过 BGP Client 中 BIRD 组件,使用 BGP 协议来传输。
kube-scheduler 负责分配调度 Pod 到集群内的节点上,它监听 kube-apiserver,查询还未分配 Node 的 Pod,然后根据调度策略为这些 Pod 分配节点(更新 Pod 的 NodeName 字段)。
kube-scheduler 分为两个阶段,predicate(预选)和 priority(优选)。
每个阶段中会有很多策略,策略是以插件的形式集成在 kube-scheduler,也可以自己编写策略。
Kubernetes 创建 Pod 时就给它指定了下列一种 QoS 类:Guaranteed
,Burstable
,BestEffort
。
Qos Class优先级排名:Guaranteed > Burstable > Best-Effort
当节点资源紧缺时,优先级低的pod会最先被节点驱逐
// Framework manages the set of plugins in use by the scheduling framework.// Configured plugins are called at specified points in a scheduling context.type Framework interface {HandleQueueSortFunc() LessFuncRunPreFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod) *StatusRunPostFilterPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, filteredNodeStatusMap NodeToStatusMap) (*PostFilterResult, *Status)RunPreBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *StatusRunPostBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string)RunReservePluginsReserve(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *StatusRunReservePluginsUnreserve(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string)RunPermitPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *StatusWaitOnPermit(ctx context.Context, pod *v1.Pod) *StatusRunBindPlugins(ctx context.Context, state *CycleState, pod *v1.Pod, nodeName string) *StatusHasFilterPlugins() boolHasPostFilterPlugins() boolHasScorePlugins() boolListPlugins() *config.PluginsProfileName() string}Schedule()-->// filterg.findNodesThatFitPod(ctx, extenders, fwk, state, pod)-->// 1.filter预处理阶段:遍历pod的所有initcontainer和主container,计算pod的总资源需求s := fwk.RunPreFilterPlugins(ctx, state, pod) // e.g. computePodResourceRequest// 2. filter阶段,遍历所有节点,过滤掉不符合资源需求的节点g.findNodesThatPassFilters(ctx, fwk, state, pod, diagnosis, allNodes)-->fwk.RunFilterPluginsWithNominatedPods(ctx, state, pod, nodeInfo)-->s, err := getPreFilterState(cycleState)insufficientResources := fitsRequest(s, nodeInfo, f.ignoredResources, f.ignoredResourceGroups)// 3. 处理扩展pluginfindNodesThatPassExtenders(extenders, pod, feasibleNodes, diagnosis.NodeToStatusMap)// scoreprioritizeNodes(ctx, extenders, fwk, state, pod, feasibleNodes)-->// 4. score,比如处理弱亲和性,将preferredAffinity语法进行解析fwk.RunPreScorePlugins(ctx, state, pod, nodes) // e.g. nodeAffinityfwk.RunScorePlugins(ctx, state, pod, nodes)-->// 5. 为节点打分f.runScorePlugin(ctx, pl, state, pod, nodeName) // e.g. noderesource fit// 6. 处理扩展pluginextenders[extIndex].Prioritize(pod, nodes)// 7.选择节点g.selectHost(priorityList)sched.assume(assumedPod, scheduleResult.SuggestedHost)--> // 8.假定选中podsched.SchedulerCache.AssumePod(assumed)-->fwk.RunReservePluginsReserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)-->f.runReservePluginReserve(ctx, pl, state, pod, nodeName) // e.g. bindVolume。其实还没大用runPermitStatus := fwk.RunPermitPlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)-->f.runPermitPlugin(ctx, pl, state, pod, nodeName) // empty hookfwk.RunPreBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) // 同 runReservePluginReserve// bind// 9.绑定podsched.bind(bindingCycleCtx, fwk, assumedPod, scheduleResult.SuggestedHost, state)-->f.runBindPlugin(ctx, bp, state, pod, nodeName)-->b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{})-->return c.client.Post().Namespace(c.ns).Resource("pods").Name(binding.Name).VersionedParams(&opts, scheme.ParameterCodec).SubResource("binding").Body(binding).Do(ctx).Error()
]]>
Linux Namespace 种类
Namespace常用操作
# 查看当前系统的 namespacelsns -t $Type# 查看某进程的 namespacels -la /proc/<pid>/ns/# 进入某 namespace 运行命令nsenter -t $PID -n $Command# 获取容器的Piddocker inspect $PID |grep -i pid# 交互式进入进程的 namespacensenter --target $PID --mount --uts --ipc --net --pid
/sys/fs/cgroup
中查看cgroup使用情况。
CPU子系统
cfs_quota_us /cfs_period_us
是进程最大可使用的CPU
CPU子系统练习
# demo程序package mainfunc main() {go func() {for{}} ()for{}}# 在 cgroup cpu 子系统目录中创建目录结构cd /sys/fs/cgroup/cpumkdir cpudemocd cpudemo# 运行 demo程序,CPU占用200%# 通过 cgroup限制 cpu## 把进程添加到cgroup进程配置组echo $PID > cgroup.procs## 设置cpuquotaecho 10000 > cpu.cfs_quota_us# CPU使用率为10%
Memory子系统
Union FS
Docker 的文件系统
典型的 Linux 文件系统组成:
Docker 复用 Linux Bootfs,自己实现 rootfs。
Docker启动
理解OverlayFS
Overlay 只有两层:upper 层和 lower 层,Lower 层代表镜像层,upper 层代表容器可写层。
OverlayFS文件系统练习
# 练习一mkdir upper lower merged workecho "from lower" > lower/in_lower.txtecho "from upper" > upper/in_upper.txtecho "from lower" > lower/in_both.txtecho "from upper" > upper/in_both.txt# 将lower、upper合并挂载sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work `pwd`/merged# 练习二# 创建练习容器docker run -d nginxdocker inspect $ContainerID# 查看容器的lower、upper、merge"GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/77a18594ec28fa3eeecd1bdcd44f05bec8111ba78eca3be65914933120165aaa-init/diff:/var/lib/docker/overlay2/80bf504e4d808700fa46ed8ca759baa26cb4a81ebe3ff7b9ab5552604013a050/diff", "MergedDir": "/var/lib/docker/overlay2/77a18594ec28fa3eeecd1bdcd44f05bec8111ba78eca3be65914933120165aaa/merged", "UpperDir": "/var/lib/docker/overlay2/77a18594ec28fa3eeecd1bdcd44f05bec8111ba78eca3be65914933120165aaa/diff", "WorkDir": "/var/lib/docker/overlay2/77a18594ec28fa3eeecd1bdcd44f05bec8111ba78eca3be65914933120165aaa/work" }, "Name": "overlay2" }# LowerDir是构建镜像的文件目录,UpperDir是容器运行后写入的文件目录,MergedDir是LowerDir+UpperDir# df -hT 能看到很多merged的挂载
Containerd 是通过创建子进程的方式管理容器,这样有个好处,就是containerd 重启不会影响所有容器。如下:
[root@zzzzz go]# docker inspect bb833c7f0ca2|grep -i pid "Pid": 1936784, "PidMode": "", "PidsLimit": null,[root@zzzzz go]# ps -ef |grep 1936784root 1936784 1936766 0 01:26 ? 00:00:00 nginx: master process nginx -g daemon off;101 1936838 1936784 0 01:26 ? 00:00:00 nginx: worker process101 1936839 1936784 0 01:26 ? 00:00:00 nginx: worker process[root@zzzzz go]# ps -ef |grep 1936766root 1936766 7452 0 01:26 ? 00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/bb833c7f0ca2551f5fdfbda4ace233c950ba4d27e88acf9f5d4c04c1f852f4e9 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc[root@zzzzz go]# ps -ef |grep 7452root 7452 1 0 Mar04 ? 00:33:36 /usr/bin/containerd
]]>
- sync.Mutex 互斥锁
- sync.RWMutex 读写分离锁
- sync.WaitGroup
- sync.Once
- sync.Cond
对象大小定义
- 小对象大小:0~256KB
- 中对象大小:256KB~1MB
- 大对象大小:>1MB
- 小对象的分配流程
- ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache 缓存都是足够的,不需要去访问CentralCache 和 HeapPage,无系统调用配合无锁分配,分配效率是非常高的
- 中对象分配流程
- 直接在 PageHeap 中选择适当的大小即可,128 Page 的 Span 所保存的最大内存就是 1MB
- 大对象分配流程
- 从 large span set 选择合适数量的页面组成 span,用来存储数据
Golang GC 的大部分处理是和用户代码并行的