开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

数据链路层(二)

发表于 2021-11-22

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战

11. 共享通信媒体

静态划分信道

  频分复用

  时分复用

  波分复用

  码分复用

动态媒体接入控制(多点接入)

  随机接入(主要被以太网采用)
  受控接入,如多点线路探寻(polling),或者轮询。(目前已不采用)

12. 以太网

最初的以太网是将许多计算机都连接到一根总线上。当初认为这样的连接方法既简单又可靠,因为总线上没有有源器件。

总线上的每一个工作的计算机都能检测到B发送的数据信号。

由于只有计算机D的地址与数据帧首部写入的地址一致,因此只有D才接受这个数据帧。

其他所有的计算机(A,C和E)都检测到不是发送给他们的数据帧,因此就丢弃这个数据帧而不能够收下来。

具有广播特性的总线上实现了一对一的通信。

1) 、载波监听多点接入/碰撞检测 以太网使用CSMA/CD协议

CSMA/CD表示carrier sense multiple access with collision detection 。

“多点接入”表示许多计算机以多点接入的方式连接在一根总线上。

“载波监听”是指每一个站在发送数据之前先要检测一下总线上是否有其他计算机在发送数据,如果有,则暂时不要发送数据,以免发送碰撞。

“载波监听”就是用电子技术检测总线上有没有其他计算机发送的数据信号。

2)、 碰撞检测

“碰撞检测”就是计算机边发送数据边检测信道上的信号电压大小。

  当有几个站童年故事在总线上发送数据时,总线上的信号电压摆动值会增大(相互叠加)。

  当一个站监测到的信号电压摆动值超过一定的门限值时,就认为总线上至少有两个站同时在发送数据,表明产生了碰撞。

  所谓“碰撞”就是发生了冲突。因此“碰撞检测”也成为“冲突检测”。

检测到碰撞后

  在发送碰撞时,总线上传输的信号产生了严重的失真,无法从中恢复出有用的信息来。

  每一个正在发送数据的站,一旦发现总线上出现了碰撞,就要立刻停止发送,免得继续浪费网络资源,然后等待一段随机时间后再发送。

3)、 传播时延对载波监听的影响

image.png
4)、 重要特性

使用CSMA/CD协议的以太网不能进行全双工通信而只能进行双向交替通信(半双工通信)。

每一个站的发送数据之后的一小段时间内,存在着遭遇碰撞的可能性。

这种发送的不确定性,使整个以太网的平均通信量远小于以太网的最高数据率。

5)、 争用期

最先发送数据帧的站,在发送数据帧后之多经过时间2τ(两倍的端到端往返时延)就可知道发送的数据帧是否遭受了碰撞。

经过争用期这段时间还没有检测到碰撞,才能肯定这次发送不会发送碰撞。

以太网的争用期

  以太网的端到端往返时延2τ成为争用期,或碰撞窗口。通常去51.2μs为争用期的长度
  对于100Mb/s的以太网,在征用期内可发送512bit,即64字节。

  以太网在发送数据时,若前64字节未发送冲突,则后续的数据也不会发生冲突。

最短有效帧长

  如果发送冲突,就一定是在发送的前64字节之内。

  由于一检测到冲突就立即终止发送,这时已经发送出去的数据一定小于64字节。

  以太网规定了最短有效帧长为64字节,凡是长度小于64字节的帧都是由于冲突而异常终止的无效帧。

6)、 二进制指数类型退避算法

发送碰撞的站在停止发送数据后,要推迟(退避)一个随机事件才能在发送数据。
确定基本退避时间,一般是争取为争用期2τ。

  定义参数k,

         k=Min[重传次数,10]

  从整数集合[0,1,2……,(2k-1)] (2的k次方减一)中随机地取出一个值,记为r。重传所需的时延就是r倍的基本退避时间。

当重传达16次扔不能成功时即丢弃该帧,并向高层报告。

7)、以太网的两个标准

  DIX Ethernet V2 是世界上第一个局域网产品(以太网)的规约。

  IEEE的802.3标准。

  DIX Ethernet V2标准与IEEE的802.3标准只有很小的差别,因此可以将802.3局域网简称为“以太网”,严格来说,“以太网”应当是指符合DIX Ethernet V2标准的局域网。

8)、 以太网与数据链路层的两个子层

为了使数据链路层能更好地适应多种局域网标准,802委员会就将局域网的数据链路层拆成两个子层:

  逻辑链路控制LLC(Logical Link Control)子层

  媒体接入控制MAC(Medium Access Control )子层

与接入到传输媒体有关的内容都放在MAC子层,而LLC子层则与传输媒体无关,不管采用何种协议的局域网对LLC子层来说都是透明的。

由于TCP/IP体系经常使用的局域网是DIX Ethernet V2而不是802.3标准中的几种局域网,因此现在802委员会制定的逻辑链路层控制子层LLC(既802.2标准)的作用已经不打了。

很多厂商生产的适配器上就仅装有MAC协议而没有LLC协议。

9)、 以太网提供的服务

以太网提供的服务是不可靠的交付,即尽最大努力的交付。

当接入站收到有差错的数据帧时就丢掉此帧,其他什么也不做差错的纠正由高层来决定。

如果高层发现丢失了一些数据而进行重传,但以太网并不知道这是一个重传的帧,而是当做一个新的数据帧来发送。

10)、 星型拓扑

传统的以太网最初是使用粗同轴电缆,后来演进到使用比较便宜的细同轴电缆,最后发展为使用更便宜和更灵活的双绞线。不用电缆而使用无屏蔽双绞线。每个站点需要用两对双绞线,分别用于发送和接收。

这种以太网采用星型拓扑,在星型的中心则增加了一种可靠性非常高的设备,叫做集线器(hub)。

11)、 集线器的一些特点

集线器是使用电子器件来模拟实际电缆线的工作,因此整个系统仍然像一个传统的以太网那样运行。集线器使用了大规模集成电路芯片,因此这样的硬件设备的可靠性已大大提高了。

使用集线器的以太网在逻辑上仍是一个总网线,各工作站使用的还是CSMA/CD协议,并共享逻辑上的总线。

集线器很像一个多接口的转发器,工作在物理层。

image.png
12)、 10Base-T

10Base-T的通信距离稍短。每个站点到集线器的距离不超过100m。

这种10Mb/s速率的无屏蔽双绞线星型网的出现,即降低了成本,又提高了可靠性。

10Base-T双绞线以太网的出现,是局域网发展史上的一个非常重要的里程碑,它为以太网在局域网中的统治地位奠定了牢固的基础。

其他:100Base-FX、100Base-T和100Base-T4…

13)、 以太网的信道利用率

以太网的信道被占用的情况:

  争用期长度为2τ,即端到端传播时延的两倍。检测到碰撞后不发送干扰信号。

  帧长为L(bit),数据发送速率为C(b/s),因而帧的发送时间为L/C=T(s)。

  一个帧从开始发送,经可能发生的碰撞后,将在重传数次,到发送成功且信道转为空闲(即再经过时间τ使得信道上无信号在传播)时为止,是发送一帧所需的平均时间。

image.png

14)、 以太网的信道利用率:参数a

要提高以太网的信道利用率,就必须减小τ与T之比。在以太网中定义了参数a,他是以太网单程端到端时延τ与真的发送时间T之比:

                            a=τ/T

    a→0表示一发生碰撞就立即可以检测出来,并立即停止发送,因而信道利用率很高。

    a越大,表明争用期所占的比例增大,没发生一次碰撞就浪费许多信道资源,使得信道利用率明显降低。

15)、 以太网的信道利用率:最大值

对以太网参数的要求

    当数据率一定时,以太网的连线的长度受到限制,否则τ的数值会太大

    以太网的帧长不能太短,否则T的值会很小,使a值太大。

信道利用率的最大值

    在理想化的情况下,以太网上的各站发送数据都不会产生碰撞(这显然已经不是CSMA/CD,而是需要使用一种特殊的调度方法),即总线一旦空闲就有某一个站立即发送数据。

    发送一帧占用线路的时间是T+τ,而帧本身的发送时间是T1。于是我们可以计算出理想情况下的极限信道利用率为:

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

python爬虫逆向之webpack,某房产管理平台登录pa

发表于 2021-11-22

网站链接

aHR0cHM6Ly92aXAuYW5qdWtlLmNvbS9wb3J0YWwvbG9naW4=

登录抓包分析

首先输入账号密码抓包,密码输入的123456,发现有两处加密参数,如下图:

token参数:
经搜索,发现是一个接口返回的,直接请求,就能拿到想要的返回值了。

那么只剩下这个password参数了。

欢迎关注我的个人微信公众号:

在这里插入图片描述

加密位置定位

通过搜索关键词password和password:,发现并不好定位,如图:

也不是xhr,打不了xhr断点。我这里用了大佬的内存漫游来hook定位加密位置。

链接地址:github.com/CC11001100/…

当然其他方法也行。

hook到的定位位置如下:

然后我们点进去,到了这里

再进,最终定位到了关键文件和关键词”encrypt”

最后关掉hook,重新搜索关键词,定位到位置,打上断点,整体的加密逻辑应该就是这里了。

抠代码

首先复制js代码到本地,删除无用的sdk登录函数

运行,然后发现环境缺失报错

那就补环境,不难,补上window和document就OK了

接下来,因为代码是用webpack打包的,格式比较难受,可以先用window.变量,把函数导出来,这样就可以在外部直接使用了。

这里要注意这个this,本地调用指向的是window,并没有i.rsaExponent和i.rsaModulus,好在这两个值是固定的,直接在网站上拿下来写死就好了。

最后调用加密函数,就能得到想要的值了。

如有不足之处,欢迎大佬们指出,多谢观看

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

漫谈基于JMM的并发编程

发表于 2021-11-22
  1. 前言

本文以并发编程中面临的“问题“为主线,依次阐述 “问题从何而来”,“问题是如何体现的“,”问题如何解决“,文章末尾会详细分析 JMM 的核心—— happens-before 规则.

  1. 什么是JMM

JMM(Java Memory Model) 是对底层 CPU 内存模型的抽象,定义了JAVA程序在多线程环境下读写共享内存的顺序规则,规定了 volatile、原子操作、锁、final 等的内存语义,这些内存语义消除了底层CPU和编译优化带来的不确定性,是 JMM 对上层代码的程序语义保证,开发者只有了解和利用这些规则和内存语义才能正确地在 JMM 之上并发编程,否则会陷入种种怪异现象中.

  1. 硬件与编译器带来的问题

高速缓存的好处是,CPU不必频繁的读写相对缓慢的内存,积聚多次写操作在缓冲区然后批量刷新到主存,减少CPU的等待和总线占用时间,但也带来了如下问题:

  • 可见性问题,由于内存太慢,CPU引入了一层缓存,而多个CPU核心间缓存是隔离的(指L1 和 L2 Cache),于是带来了可见性性问题;

其实所谓缓存可见性问题,也可以看作是多个CPU核心间的缓存一致性问题.

  • 指令重排问题,为了提升性能,在不改变单线程程序语义(as-if-serial)的前提下,编译器和处理器可以对没有数据依赖的指令进行重排序,指令是乱序执行(out-of-order execution)的.
  • 原子性问题,CPU每一次访问内存,发起一次总线事务,当多个CPU核心同时发起总线事务时,需要进行总线仲裁(Bus Arbitration),只有其中一个可以成功,其他的需要等待,这一机制保证了总线事务的原子性. 但是,取决CPU的位数,32位CPU一次只读写32个bit, 如果要读取64个bit,则需要分成两次, 这样就需要发起两次总线事务,也就不具备原子性了. 以java中的long/double为例, 假如CPU核心A在第一个总线事务内写了long/double变量的高32位,CPU核心B在第二个总线事务内读了这个long/double变量,那么这时B就读到了一个“写了一半“的数据.

2.1 可见性问题在代码中的体现

1
2
3
4
5
6
7
8
9
java复制代码boolean flag=false
void write(){
flag=true;
}
void read(){
while(!flag){
}
System.out.println("flag has been set true");
}

想象一下,线程A先执行write,之后线程B执行read,按照程序的编码顺序来看,如果线程A已经把flag已经被写为true了,那么线程B应当会读到这flag=true,进而打印输出语句.

但实际情况是,但是线程B一直没有打印输出语句,其读到的flag值一直都是false. 这是因为线程A对flag的写操作仍旧停留在本地缓存,而没有刷新到主存(前面说到这是为了性能的优化),所以对线程B并不可见.

2.2 重排问题在代码中的体现

如下代码中,操作1和操作2之间存在数据依赖,如果重排将改变单线程程序语义,所以编译器和CPU不会重排这两个操作.

1
2
java复制代码int i =1; //1
int j=i; //2

如下代码中,操作1和操作2之间不具有数据依赖,重排后不影响单线程程序语义,所以编译器和CPU可能会重排这两个操作.

1
2
java复制代码int i=1;	//1
int j=2; //2

如下代码中,操作1与操作2之间没有数据依赖,因此可以重排; 3和4存在数据依赖因此不能重排(因为必须要先计算if表达式的值,才能确定是否jump到if块里)

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码int i=0;
boolean flag=false;
void write(){
i=1; //1
flag=true; //2
}

void read(){
if(flag){ //3
int j = i; //4
}
}

假如操作1和操作2发生重排,重排后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码int i=0;
boolean flag=false;
void write(){
flag=true; //2
i=1; //1
}

void read(){
if(flag){ //3
int j = i; //4
}
}

这种重排虽然并不影响单线程环境下的程序语义,但是并发环境下,重排前后的语义已经完全发生了变化,想象一下线程A/B并发执行,A执行write,B执行read,那么一种可能的执行时序如下:

1
2
3
4
5
6
7
8
java复制代码//A线程执行: 
flag=true; //2
//B线程执行:
if(flag){ //3
int j = i; //4
}
//A线程执行:
i=1; //1

线程B会读到flag=true,但是i的值没有变,回过头去看看重排前的代码,你会发现这种现象很“诡异”,自己写的代码明明是先写i的值,后写flag,结果却发生flag值改变了,但i的还没变的现象.

这就是重排在并发环境下带来的问题,其让程序的执行顺序处于一种不确定的状态.

2.3 原子性问题在代码中的体现

除了体现在读写数据位数超过总线位数时(如double long读写),原子性还体现read-modify-write操作时.
volatile仅能保证单次读写的原子性,并不能保证复合操作的原子性. 以如下代码为例:

1
2
3
4
5
6
java复制代码class RMWExample{
volatile int i;
void volatileIncr(){
i=i+1;
}
}

i=i+1如果编译成x86汇编,对应如下3条指令,虽然第3步的volatile写指令会加lock前缀保证原子性,但是前两个操作仍是在独立总线事务中执行的,所以这种read-modify-write类型的复合操无法用volatile保证原子性.

1
2
3
x86复制代码mov eax,1	//1
inc eax //2
lock mov eax,[i的地址] //3
  1. 如何解决问题

前面说明了可见性、有序性、原子性的问题,下面从JMM、操作系统、硬件3个层面说明如何解决这些问题.

3.1 JMM层面

硬件和编译器的优化提升了性能,但是其来带可见性和指令重排问题会让并发程序充满不确定性,为了在性能与并发不确定性之间作出权衡,JMM 规定了几种 happens-before规则 用于不同程度上消除不确定性(或者说偏序关系).

happens-beofre 一词源自论文《Time,Clocks and the Ordering of Events in a Distributed System》,用于描述分布式系统中事件之间的偏序关系(partial ordering). JMM中用happens-before描述两个操作之间的执行顺序,如果 A 操作 happens-before B 操作,那么 A 操作的执行结果对 B 操作可见,这也意味着 A 操作与 B 操作不能被重排.

JMM一共定义了7种 happens-before 关系:

  1. as-if-serial规则(也叫程序顺序规则),单个线程的每一个操作都happens-before后续的操作.

翻译成人话: 单个线程的每一个操作都对后续操作可见,
2. volatile规则,对一个volatile变量的写 happens-before于 后续的读

翻译成人话: 如果一个CPU核心A写了volatile变量,那么之后其他核心读该变量时,就必须先把核心A的缓冲区刷新到内存,以保证其它核心可以读到最新值.
3. 监视器锁规则,临界区内的操作可以按as-if-serial规则重排,但是不能重排到临界区外

想想临界区内的操作如果重排到临界区外会发生什么? 这将意味着这个操作不具有互斥性,相当于锁了个寂寞. 因此不能重排出临界区是必须的保证
4. 线程启动规则,一个线程调用Thread.start产生子线程时,父线程之前的操作 happens-before 子线程执行的任意操作.

执行子线程代码之前,必须把父线程调用Thread.start之前的所有操作刷新到主存.
5. 程序结束规则,线程中的任意操作都必须在其他线程检测到其已经结束之前执行.

一个线程检测另一个线程结束的方式可以是: Thread.join返回或者Thread.isAlive返回false
6. 传递性,A happens-before B,B happens-before C,那么A happens-before C.
7. final,对象的final域初始化操作 happens-before 对象引用被访问.

人话: 对象final域的初始化不能被重排到对象构造之外,这保证了线程拿到对象引用之前,final域已经被初始化.

单线程单线程的 happens-before 规则是编译器和CPU默认就遵守的,而多线程的 happens-before 关系则需要使用内存屏障. JMM抽象了4种内存屏障:

  • StoreStore,前面的写对后面的写可见,不可以写写重排
  • StoreLoad,前面的写对后面的读可见,不可以写读重排
  • LoadLoad,前面的读在后面的读可见,不可以读读重排
  • LoadStore,前面的读在后面的写之前,不可以读写重排

需注意,以上内存屏障只是JMM的抽象,编译时,按CPU架构的不同插入的屏障指令也不同.

内存屏障插入在两个操作之间,保证前一个操作与后一个操作的执行顺序,确定了多线程间读写操作的 happens-before 关系.

3.2 操作系统层面

内存屏障实现了多线程间的操作有序性(防重排)以及可见性,而在原子性上,CPU只能保证单条指令的原子性,如果需要保证多条指令的原子性,则需要依靠OS提供的同步支持.

linux内核提供了 futex 作为实现同步原语(synchronization primitive)的基础,高级语言的管程(Monitor)均基于这一特性(或类似的特性)实现等待唤醒. java 中的内置锁和 ReentrantLock 保证了临界区的互斥性,基于此可以实现多条指令的原子性.

3.3 硬件层面

以intel x86架构为例,JIT在编译字节码为汇编时,会给汇编复合指令加lock前缀,保证:

  1. 复合指令的原子性,通过锁总线或者cache locking手段实现

cache locking无需锁住总线,只会阻塞其他cpu核对相关内存的缓存块的访问
2. 防重排,CPU不会将“加了lock前缀的指令”与“前面或者后面的指令重排”
3. 可见性,把缓冲区中的所有数据刷新到内存中(这也意味着volatile写除自身具有可见性外,也会让前面的普通写也具有可见性)

  1. 分析 happens-before 规则

在 JVM 这个虚拟抽象的世界里,JMM 屏蔽了底层硬件的差异,抽象出各种 happens-before 规则,这些规则是 JMM 的灵魂,可以看做是 JVM 层面的“缓存一致性协议”.

4.1 as-if-serial规则

as-if-serial规则是最基本的内存语义保证,保证单线程环境下程序语义不变,也就是说只要操作不具有依赖性,编译器和CPU就可以重排.

4.2 volatile规则

如何保证可见性?

当写一个volatile变量时,会锁总线,然后直接写主存,并且将线程本地内存中的所有共享变量也一同刷新到主存,将其他线程对应的本地内存设置为失效.
当读一个volatile变量时,由于缓存失效,线程就会去主存读.

如何保证原子性?

当写一个volatile变量时,会锁总线,避免其他核心读写内存. 如果是在 32-bit CPU 上实现 64-bit 数据的读写原子性,由于读或写要分两条指令进行,需要通过锁来实现临界区.

根据本人查阅的资料显示,有的32位CPU也有64位寄存器,也可以通过一条指令实现读写原子性,不一定需要os以上层面的锁. 不过这块没有深究,暂且退一步来看,在功能上高级语言层面的锁也可以保证原子性,当然缺点是有系统调用,阻塞线程,切换上下文的开销.

如何防重排?

  • 在volatile写操作之前加入StoreStore,保证普通写不能与volatile写重排;之后加入StoreLoad屏障,保证后面的volatile读写不能和当前volatile写重排.
  • 在volatile读操作之后加入LoadLoad和LoadStore屏障,分别保证后面的普通读和写操作不能与volatile读重排.

前文的 重排问题在代码中的体现 的最后一个例子中,如果给flag加上volatile,则可以保证1和2,以及3和4不被重排:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码int i=0;
volatile boolean flag=false;
void write(){
//StoreStore 上面的普通写不能与volatile写(操作2)重排
flag=true; //2
//StoreLoad 后面的volatile读写不能和volatile写(操作2)重排
i=1; //1
}

void read(){
if(flag){ //3
//LoadLoad 后面的普通读操作不能与volatile读(操作3)重排
//LoadStore 后面的普通写操作不能与volatile读(操作3)重排
int j = i; //4
}
}

以上只是概念上的解释,仅用作帮助理解volatile,事实上“一个读写操作”和“一行代码语句或表达式”是两码事.

4.3 锁规则

概念上讲,锁是一种高级抽象,它使用了硬件层面的原子指令来实现state的变更,和OS层面的条件变量(condvar,如linux中的futex)来实现线程等待与唤醒.

AQS

Java 中每一个线程在 JVM 层面均对应一个 condvar, 当调用 LockSupport.park 和 LockSupport.unpark 时,底层通过 condvar 来实现线程的等待和唤醒.

AQS 中独立维护了 state 和等待队列,当需要让当前线程阻塞时,会将线程加入等待队列,并调用 LockSupport.park(Object blocker) 阻塞当前线程.
而当要唤醒一个线程时,会从 AQS 队列(或 ConditionObject 队列)中找到要被唤醒的线程,调用LockSuport.unpark(Thread thread) 让 JVM 将这个线程唤醒.

以 ReentrantLock 为例,调用 lock 时会调用 AQS 对象的 acquire ,如果 tryAcquire 失败的话,那么就会调用 addWaiter 将线程加入 AQS 队列,并且调用 LockSupport.park() 进入 JVM 层面,随后线程对象被加入 condvar 的等待队列,至此一个获取锁失败的线程就这样被阻塞了.

而当线程调用 unlock 时,会调用 release() 然后在 unparkSuccessor 里找出 AQS 队列里要被唤醒的线程(也就是之前获取锁失败而阻塞的线程),最后调用 LockSuport.unpark(Thread thread) ,由 JVM 从 condvar 队列中唤醒线程.

ReentrantLock#newCondition() 返回的 ConditionObject 的 await、signal 原理也上述过程类似.

AQS 的实现有一点反直觉的是,condvar 和 线程一一对应,而不是和 AQS,同步器只负责维护 state 和链表,当需要 blocking/unblocking 一个线程时,通过该线程关联的 condvar 来实现.

内置锁

按照 ReentrantLock 类的说明,内置锁与其在行为和语义上是一致的,就不多介绍了.

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.

锁是如何保证3要素的?

  • 原子性,通过阻塞线程来实现临界区只有一个线程访问
  • 可见性和有序性,站在 JMM 的抽象层面来看,临界区会加入屏障,保证临界区内的操作不会重排到外面. 从 CPU 层面看, lock 和 unlock 操作都会产生 CAS 操作(因为要更新条件变量),而这会用到 lock 指令,相当于屏障的作用,因此临界区内的指令不会重排到外面,并且对后续操作可见.

4.4 Thread#start()规则

这个规则保证的是主线程在创建子线程之前的操作都对子线程的操作可见,可以看作是在子线程的所有操作之前加入 StoreLoad 屏障,保证主线程的写已经 cache flush.

1
2
3
4
5
6
7
java复制代码//主线程的操作
int i = 1;
new Thread(()->{
//子线程的操作
//这里插入一条 StoreLoad
int j = i;
}).start()

4.5 Thread#join()规则

确保子线程的修改在被其他线程观测到结束前已经 cache flush,可通过在子线程的指令序列末尾加入一条 StoreLoad 屏障来实现.

1
2
3
4
5
6
7
java复制代码int [] arr = {1};
Thread t = new Thread(()->{
arr[0]=2
//这里插入一条 StoreLoad
});
t.join();
int r = arr[0];

4.6 原子类规则

硬件层面

从X86汇编的层面看,原子操作是通过给复合指令加lock前缀来保证的,有些复合指令,比如 cmpxchg,本身不具有原子性,执行时会产生多个总线事务(意味着会经历多次总线的占有和释放). 如果加上lock前缀,多个总线事务就会合并为一个,意味着执行期间会让CPU核心一直锁住总线(或者cache),这也就避免了其他CPU核心在期间修改共享数据. CAS(Compare An Swap)就是通过lock cmpxchg实现的.

JAVA层面

java.util.concurrent.atomic 包中的所有原子类,都是基于native方法Unsafe.compareAndSwapXX(obj,offset,expect,update),底层基于CPU提供的原子指令.

offset参数是通过Usafe#xxOffset()得到的,是如果要改的是对象字段,那么就是数据在对象堆内偏移,如果要改的是数组元素,那么offset就是在数组堆内的偏移

使用CAS可以用于解决以上问题,概念上java实现代码如下:

1
2
3
4
5
6
7
java复制代码class RMWExample{
int i;
void volatileIncr(){
//对i字段进行原子更新
Unsafe.compareAndSwapInt(i,iOffset,1,2);
}
}

iOffset是i字段的在对象堆内的偏移,通过 Unsafe.getUnsafe().objectFieldOffset()可以得到.

以上代码编译成X86汇编后如下:

1
2
3
4
less复制代码mov edx,i的地址		//1.将i的内存地址读到edx寄存器
mov ecx,2 //2.将要更新的值放入ecx寄存器
mov eax,1 //3.将要比较的值放入eax寄存器
lock cmpxchg dword ptr [edx],ecx //4.通过i的地址读到内存中的i最新值,与eax寄存器的值(也就是expectValue)相比较,如果一致就写入ecx的值(也就是updateValue).

第4步中cmpxhg是一个if then do(或者说read-modify-write)的复合操作,本身不具有原子性,但是加了lock前缀以后,硬件层面会锁住总线(或者cache locking)来阻塞其他CPU核心对数据的访问,保证了原子性.

前面说过,lock 指令具有内存屏障的作用,这也意味着除了原子性,CAS操作还具有可见性和有序性的内存语义.

ABA问题

CAS存在一个问题是,仅仅比较值是否是旧的,而不检查是否在上次访问之后被改过,比如当前线程一开始读到是A,此后其他线程写B后又写为A,那么当前线程在CAS时可以成功(因为值最终还是A).这就是ABA问题,这种情况在有些场景下是不可容忍的.
为了解决ABA问题,可以每次写值时都写一个唯一的版本号作为区分,这样ABA中的前一个A和后一个A虽然值一样但版本就不同.java中提供了AtomicStampedReference作为实现.

4.7 final规则

从程序语义上来看,final变量在构造对象时赋值,之后每次读到的值都应该是一样的. 但是在 JSR-133 以前却可能发生final变量的值被观测到变化的现象.

1
2
3
4
5
6
java复制代码public class FinalExample{
final int finalVar;
public FinalExample(){
finalVar=1;
}
}
1
2
3
java复制代码//线程A初始化对象
FinalExample finalExample = new FinalExample();
//将 finalExample 引用传给线程池的线程去访问(省略具体代码)

以上例子中,线程A初始化FinalExample对象,并把引用传给线程B.

在旧的内存模型中, final 变量并没有特别的内存语义,编译器和CPU只要遵守 as-if-serial语义,就可以随意重排. 以上代码重排前后的操作序列如下:

JMM-final-0.png

如上图所示,写final域 被重排到了 读对象引用 之后. 在并发环境下,线程A把对象引用传给线程B,然后线程B 先后读两次 final 变量, 这个过程可能产生如下执行序列:

JMM-final-1.png

线程 B 第一次读 final 域读到了初始化前的默认值,线程A 写 final 域之后,线程B第二次读 final 域又读到初始化后的值. 在这个过程中,站在线程 B 的视角,final 域,这个本不可变的字段却被读出两次不一样的值,这违背了 final 关键字的程序语义.

JSR133 增强了 final 域的内存语义,编译器在写 final 域操作之后,到构造函数返回之前,会插入一条 StoreStore 屏障,这保证了 final 域的写会在对象引用被写入某个变量前,CPU 会将 cache flush到主存.

JMM-final-2.png

FinalExample finalExample = new FinalExample(); 可分为两步: 1.执行构造初始化对象堆 2.然后将堆地址写入变量, 构造返回前的 StoreStore 可以确保在第二步之前 final 变量已经 cache flush.

参考文献

  • 《JAVA并发编程的艺术》
  • 《JAVA并发编程实践》
  • 《现代操作系统——原理与实现》
  • AQS源码

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Windows远程文件拷贝到openEuler--WinSC

发表于 2021-11-22

​这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」​

由于最近项目迁移到openEuler操作系统上去了、需要部署安装项目、各种软件环境安装、资料拷贝复制等、记得以前用过WinSCP进行远程数据拷贝。今天就简单记录下使用方法。

百度百科介绍:

WinSCP是一个Windows环境下使用SSH的开源图形化SFTP客户端。同时支持SCP协议。它的主要功能就是在本地与远程计算机间安全的复制文件。.winscp也可以链接其他系统,比如linux系统。

基本特性

图形用户界面、多语言 [1]、与Windows完美集成(拖拽, URL,快捷方式)支持所有常用文件操作、

支持基于SSH-1、SSH-2的SFTP和SCP协议 [2]支持批处理脚本和命令行方式、

多种半自动、自动的目录同步方式内置文本编辑器、

支持SSH密码、键盘交互、公钥和Kerberos(GSS) 验证通过与Pageant(PuTTY Agent)集成支持各种类型公钥验证、

提供Windows Explorer与Norton Commander界面、可选地存储会话信息、可将设置存在配置文件中而非注册表中,适合在移动介质上操作。

操作介绍

WinSCP可以执行所有基本的文件操作,例如下载和上传。同时允许为文件和目录重命名、改变属性、建立符号链接和快捷方式。

两种可选界面允许用户管理远程或本地的文件。连接到远程计算机

使用WinSCP可以连接到一台提供SFTP (SSH File Transfer Protocol)或SCP (Secure Copy Protocol)服务的SSH (Secure Shell)服务器,通常是UNIX服务器。SFTP包含于SSH-2包中,SCP在SSH-1包中。两种协议都能运行在以后的SSH版本之上。WinSCP同时支持SSH-1和SSH-2。

安装方式:

官网下载地址:winscp.net/eng/docs/la… 下载后直接安装完成。

)​

远程连接:

安装完成之后运行程序打开链接、输入IP地址、账号密码信息进行链接。

)​

使用

这是连接成功之后的数据拷贝、点击上传即可传输到远程服务器上。 也可以由远程服务器下载

​大家点赞、收藏、关注、评论啦 、

打卡 文章 更新 104/ 365天

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

weakHashMap 源码分析

发表于 2021-11-22

java中4种引用

强引用:
强引用是默认支持,当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,
就算是出现了OOM也不会回收对象。

软引用
一种相对强引用弱化了一些的引用,用java.lang.ref.SoftReference实现,
可以让对象豁免一些垃圾收集。当系统内存充足的时候,不会被回收;
当系统内存不足的时候,会被回收

弱引用

弱引用需要用java.lang.ref.WeakReference实现,它比软引用的生存期更短,
对于弱引用的对象来说,
只要垃圾回收机制一运行,不管JVM的内存空间是否够,
都会回收该对象的占用内存。

虚引用

虚引用要通过java.lang.ref.PhantomReference类来实现,
虚引用不会决定对象的生命周期,如果一个对象只有虚引用,就相当于没有引用,
在任何时候都可能会被垃圾回收器回收。它不能单独使用也不能访问对象,
虚引用必须和引用队列联合使用。

引用类关系

java引用.jpg

expungeStaleEntries entry 对象回收方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ini复制代码private void expungeStaleEntries() 
{
for (Object x; (x = queue.poll()) != null; )
{
// 遍历链表中的元素是否在队列中 如果在 把value =null
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);

Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
// 数量-1
size--;
break;
}
prev = p;
p = next;
}
}
}
}

get 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public V get(Object key) 
{
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int index = indexFor(h, tab.length);
//step1 获取数组的值
Entry<K,V> e = tab[index];
while (e != null)
{
// 如果hash值 key 相同 然后返回value
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
// 否则找不到
return null;
}

maskNull 判断key 是否是Null

1
2
3
4
5
6
vbnet复制代码private static Object maskNull(Object key) 
{
// 空object对象 // 如果是 key 转为 new Object()
private static final Object NULL_KEY = new Object();
return (key == null) ? NULL_KEY : key;
}

hash 散列函数

1
2
3
4
5
6
7
8
9
scss复制代码final int hash(Object k) {
int h = k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

getTable 获取数组 Entry<key,value> 数组

1
2
3
4
5
scss复制代码private Entry<K,V>[] getTable() {
// 回收 value对象
expungeStaleEntries();
return table;
}

indexFor 获取存放数组的位置

1
2
3
4
5
arduino复制代码private static int indexFor(int h, int length) 
{
// h key hash 值 length 数组的个数
return h & (length-1);
}

put key-value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ini复制代码public V put(K key, V value) 
{
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
// 找到存放的数组下标位置
for (Entry<K,V> e = tab[i]; e != null; e = e.next)
{
// 如果当前位置有值 hash 冲突了 然后判断key和hash 值是否相同
// 如果相同 替换value值
if (h == e.hash && eq(k, e.get()))
{
V oldValue = e.value;
if (value != oldValue)
// 如果不相等 替换value
e.value = value;
// 返回原来的值 替换前的value值
return oldValue;
}
}

modCount++;
// 如果这个位置没有值
Entry<K,V> e = tab[i];
// 创建一个Entry对象 然后tab[i]保存这个对象
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
// 扩容
resize(tab.length * 2);
return null;
}

Entry

1
2
3
4
5
6
7
8
9
10
ini复制代码Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
// key 弱引用 不是value 切记
// 他是判断key 是否被回收了 如果回收了 把value =null
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用腾讯云轻量服务器搭建一个漂亮的导航主页

发表于 2021-11-22

经常看到别人有一个漂亮的导航主页而羡慕不已,想自己也搭建一个,但是只能留下没技术的泪水。今天无意间看到了一个开源的主页项目,看起来非常的漂亮,可以高度自定义,而且还提供了docker镜像,安装起来也十分的方便。

QQ截图20211122144045.png

刚好经过了腾讯云的双十一大促,手里又多了几台吃灰的轻量服务器,就用其中一台来搭建了一个。下面简单说一下搭建的教程

购买服务器

如果没有服务器的话,需要先购买一台腾讯云的轻量服务器。

轻量应用服务器(TencentCloud Lighthouse)是新一代开箱即用、面向轻量应用场景的云服务器产品,助力中小企业和开发者便捷高效的在云端构建网站、小程序/小游戏、电商、云盘/图床以及各类开发测试和学习环境,相比普通云服务器更加简单易用,提供高带宽流量包并以套餐形式整体售卖基础云资源,将热门开源软件融合打包实现一键构建应用,是您使用腾讯云的最佳入门途径。为了方便安装应用,我们需要准备Docker环境。

腾讯云经常举办各种活动,可以点击这里进入他们的活动页面进行购买,可以选择距离最近的区域购买,不过不想备案的话,可以选择境外的服务器(比如香港区域)进行购买。镜像的话,选择【官方镜像】下的【docker基础镜像】,实例套餐选择适合自己的,然后提交订单,付款即可。如果不选择【Docker基础镜像】,后面需要自行安装docker容器。

QQ截图20211122143655.png

安装docker

如果没有选择docker基础镜像,则需要自行安装docker镜像。这里假设使用的Ubuntu系统。安装docker的步骤如下

1
复制代码

sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
curl -fsSL download.docker.com/linux/ubunt… | sudo gpg –dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo

“deb [arch=(dpkg−−print−architecture)signed−by=/usr/share/keyrings/docker−archive−keyring.gpg]https://download.docker.com/linux/ubuntu (dpkg –print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu
(dpkg−−print−architecture)signed−by=/usr/share/keyrings/docker−archive−keyring.gpg]https://download.docker.com/linux/ubuntu (lsb_release -cs) stable” | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

1
复制代码

安装Homer

由于这个主页项目提供了docker镜像,因此安装起来非常简单,只需要一行命令即可。为了方便配置主页上的信息和上传图片,我们需要把配置文件的目录挂载到宿主机的目录上来方便我们修改。命令如下

1
2
3
bash复制代码mkdir -p /data/homepage/assets   #我是把配置文件挂载到宿主机的这个目录下,也可以指定到自己喜欢的目录

docker run -d -p 8080:8080 -v /data/homepage/assets:/www/assets --restart always b4bz/homer:latest # 然后启动Docker容器即可

启动成功后,在浏览器中访问,如果能看到页面打开,说明已经成功了一半了。这一步一般不会有什么问题。由于我们还没有进行任何的配置,因此,这个地方看到的是默认的主页。下面修改配置文件来将其改成自己的内容。

配置Homer

进入到/data/homepage/assets目录下(如果把配置文件没有挂载到这个目录下,就到自己挂载的目录下去看),可以看到里面多了一些文件和文件夹,我们重点关注的是config.yml文件夹和tools文件夹,config.yaml用来配置页面上显示的内容,而tools文件夹用来放页面上需要的图片资源。用自己熟悉的文本编辑工具打开config.yml进行编辑。里面的内容也非常简单,一看就明白了。下面的是我编辑的内容,编辑好后保存。

logo的话需要图片资源,把图片放到tools目录下,然后在配置文件中,用”assets/tools/xxxx.png”进行引用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
yaml复制代码# Homepage configuration
# See https://fontawesome.com/icons for icons options

title: "我的个人主页"
subtitle: "Homer"
logo: "logo.png"
# icon: "fas fa-skull-crossbones" # Optional icon

header: true
footer: '收集一些有趣的网站' # set false if you want to hide it.

# Optional message
message:
#url: https://b4bz.io
style: "is-dark" # See https://bulma.io/documentation/components/message/#colors for styling options.
title: "公告"
icon: "fa fa-grin"
content: "欢迎访问我的个人主页,商务合作请联系我的邮箱ceo@gmail.com"

# Optional navbar
# links: [] # Allows for navbar (dark mode, layout, and search) without any links
links:
- name: "我的博客"
icon: "fab fa-blog"
url: "https://www.lixf.cc"
target: "_blank" # optional html a tag target attribute
- name: "我的github"
icon: "fas fa-book"
url: "https://github.com/lixiaofei123"

# Services
# First level array represent a group.
# Leave only a "items" key if not using group (group name, icon & tagstyle are optional, section separation will not be displayed).
services:
- name: "社交网站"
items:
- name: "Awesome app"
logo: "assets/tools/sample.png"
subtitle: "Bookmark example"
tag: "app"
url: "https://www.reddit.com/r/selfhosted/"
target: "_blank" # optional html a tag target attribute
- name: "Another one"
logo: "assets/tools/sample2.png"
subtitle: "Another application"
tag: "app"
url: "#"
- name: "建站必备"
items:
- name: "站长工具"
logo: "assets/tools/chinaz.png"
subtitle: "集合了常见的站长工具"
url: "https://tool.chinaz.com/"
target: "_blank"
- name: "51脚本"
logo: "assets/tools/jb.png"
subtitle: "找脚本,来这里就对了"
target: "_blank"
url: "https://www.51jb.net"
- name: "腾讯云"
logo: "assets/tools/qqcloud.png"
subtitle: "国内领先的云服务提供商"
target: "_blank"
url: "https://curl.qcloud.com/U8WTYLwt"

配置完毕后,无需重启docker容器,刷新页面就可以看到我们修改的内容生效了。

QQ截图20211122145301.png

总结

总的来说,这个安装配置还是很容易的,小伙伴们快试试吧

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里大规模业务混部下的全链路资源隔离技术演进 混部和资源隔离

发表于 2021-11-22

简介: 本文作为混部实践系列开篇,本篇文章将介绍资源隔离技术在混部中的重要性、其落地挑战及我们的应对思路。

作者:钱君、南异

混部顾名思义,就是将不同类型的业务在同一台机器上混合部署起来,让它们共享机器上的 CPU、内存、IO 等资源,目的就是最大限度地提高资源利用率,从而降低采购和运营等成本。

2014 年,阿里开始了第一次探索混部,经过七年磨练,这把将资源利用率大幅提升的利剑正式开始商用。

通过计算资源、内存资源、存储资源、网络资源等全链路的隔离以及毫秒级的自适应调度能力,阿里可以在双十一的流量下进行全时混部,通过智能化的决策与运维能力,支撑着内部百万级的 Pod 混部,不管是 CPU 与 GPU 资源,普通容器与安全容器,包括国产化环境各种异构基础设施,都能实现高效混部,这让阿里核心电商业务生产集群成本下降了 50% 以上,同时让核心业务受到的干扰小于 5%。

针对云原生时代的资源效能提升问题,我们将基于大规模场景下的混部实践推出系列文章,详细介绍并分享关于混部技术的细节,及大规模生产中碰到的种种落地的实际问题。作为系列开篇,本篇文章将介绍资源隔离技术在混部中的重要性、其落地挑战及我们的应对思路。

混部和资源隔离之间的关系:资源隔离是混部的基石

混部通常是将不同优先级的任务混合在一起,例如高优先级的实时任务(对时延敏感,资源消耗低;称为在线)和低优先级的批处理任务(对时延不敏感,资源消耗高;称为离线),当高优先级业务需要资源时,低优先级任务需要立即归还,并且低优先级任务的运行不能对高优先级任务造成明显干扰。

为了满足混部的需求,在单机维度的内核资源隔离技术是最为关键的一项技术,阿里云在内核资源隔离技术上深耕多年,积累了许多业界领先的经验,我们将这些内核资源隔离技术主要涉及的范围概括为内核中的调度、内存和 IO 这三大子系统,并且在各个子系统领域根据云原生的混部场景进行了深入的改造和优化,包括 CPU Group Identity、SMT expeller、基于 Cgroup 的内存异步回收等。这些关键的技术使客户有能力在云原生混部场景中根据业务特点给出最优解决方案,有效提高用户的资源使用率并降低用户资源的使用成本,非常适用于容器云混部场景,同时也是大规模化混合部署方案所强依赖的关键技术。

下图是资源隔离能力在整个混部方案中的位置:

为什么需要资源隔离,资源隔离会遇到哪些拦路虎

假设我们现在有一台服务器,上面运行了高优的在线业务以及离线任务。在线任务对响应时间 (Response Time, RT) 的需求是很明确的,要求尽可能低的 RT,故被称之为延迟敏感型 (Latency-Sensitive, LS) 负载;离线任务永远是有多少资源吃多少资源的,故此类负载被称之为 Best Effort (BE)。如果我们对在线和离线任务不加干涉,那么离线任务很有可能会频繁、长期占用各种资源,从而让在线任务没有机会调度,或者调度不及时,或者获取不到带宽等等,从而出现在线业务 RT 急剧升高的情况。所以在这种场景下我们需要必要的手段来对在线和离线容器进行资源使用上的隔离,来确保在线高优容器在使用资源时可以及时地获取,最终能够在提升整体资源使用率的情况下保障高优容器的 QoS。

下面让我们一起看看在线和离线混跑时可能出现的情况:

  • 首先,CPU 是最有可能面对在离线竞争的,因为 CPU 调度是核心,在线和离线任务可能分别会调度到一个核上,相互抢执行时间;
  • 当然任务也可能会分别跑到相互对应的一对 HT 上,相互竞争指令发射带宽和其他流水线资源;
  • 接下来 CPU 的各级缓存必然是会被消耗掉的,而缓存资源是有限的,所以这里涉及到了缓存资源划分的问题;
  • 即使我们已经完美解决了各级缓存的资源划分,问题仍然存在。首先内存是 CPU 缓存的下一级,内存本身也类似,会发生争抢,不论在线或离线任务,都是需要像 CPU 缓存一样进行内存资源划分的;
  • 另外当 CPU 最后一级缓存 (Last Level Cache, LLC) 没有命中的时候,内存的带宽(我们称之为运行时容量,以有别于内存大小划分这种静态容量)会变高,所以内存和 CPU 缓存之间的资源消耗,是相互影响的;
  • 假设 CPU 和内存资源都没问题,对于本机来说现在隔离已经做得很好了,但是在线高优的业务和离线任务的运行过程中都是和网络有密切的关系,那么很容易理解,网络也可能是需要隔离的;
  • 最后,线上部分机型对 IO 的使用可能会发生抢占,我们需要有效的 IO 隔离策略。

以上就是一个很简单的资源隔离流程的思路,可以看到每一环都有可能会出现干扰或者竞争。

隔离技术方案介绍:独有的隔离技术方案,各显神通

内核资源隔离技术主要涉及内核中的调度、内存和 IO 这三大子系统,这些技术基于 Linux Cgroup V1 提供资源的基本隔离划分以及 QoS 保障,适用于容器云场景,同时也是大规模化混合部署方案所强依赖的关键技术。

除了基本的 CPU、内存和 IO 资源隔离技术外,我们也研发了资源隔离视图、资源监控指标 SLI (Service Level Indicator) 以及资源竞争分析等配套工具,提供包括监控、告警、运维、诊断等在内的整套资源隔离和混部解决方案,如下图所示:

弹性容器场景的调度器优化

如何保证计算服务质量的同时尽可能提高计算资源利用率,是容器调度的经典问题。随着 CPU 利用率不断提升,CPU 带宽控制器暴露出弹性不足的问题日趋严重,面对容器的短时间 CPU 突发需求,带宽控制器需要对容器的 CPU 使用进行限流,避免影响负载延迟和吞吐。

CPU Burst 技术最初由阿里云操作系统团队提出并贡献到Linux社区和龙蜥社区,分别在 Linux 5.14 和龙蜥ANCK 4.19 版本被收录。它是一种弹性容器带宽控制技术,在满足平均 CPU 使用率低于一定限制的条件下,CPU Burst 允许短时间的 CPU 突发使用,实现服务质量提升和容器负载加速。

在容器场景中使用 CPU Burst 之后,测试容器的服务质量显著提升,如下图所示,在实测中可以发现使用该特性技术以后,RT长尾问题几乎消失。

Group Identity 技术

为了满足业务方在 CPU 资源隔离上的需求,需要在满足 CPU 资源利用最大化的情况下,保证高优业务的服务质量不受影响,或将影响范围控制在一定范围内。此时内核调度器需要赋予高优先级的任务更多的调度机会来最小化其调度延迟,并把低优先级任务对其带来的影响降到最低,这是行业中通用的需求。

在这样的背景下,我们引入了 Group Identity 的概念,即每个 CPU Cgroup 会有一个身份识别,以 CPU Cgroup 组为单位实现调度特殊优先级,提升高优先级组的及时抢占能力确保了高优先级任务的性能,适用于在线和离线混跑的业务场景。当在离线混部时,可以最大程度降低由于离线业务引入的在线业务调度不及时的问题,增加高优先业务的 CPU 抢占时机等底层等核心技术保障在线业务在 CPU 调度延迟上不受离线业务的影响。

SMT expeller 技术

在某些线上业务场景中,使用超线程情况下的 QPS 比未使用超线程时下降明显,并且相应 RT 也增加了不少。根本原因跟超线程的物理性质有关,超线程技术在一个物理核上模拟两个逻辑核,两个逻辑核具有各自独立的寄存器(eax、ebx、ecx、msr 等等)和 APIC,但会共享使用物理核的执行资源,包括执行引擎、L1/L2 缓存、TLB 和系统总线等等。这就意味着,如果一对 HT 的一个核上跑了在线任务,与此同时它对应的 HT 核上跑了一个离线任务,那么它们之间是会发生竞争的,这就是我们需要解决的问题。

为了尽可能减轻这种竞争的影响,我们想要让一个核上的在线任务执行的时候,它对应的 HT 上不再运行离线任务;或者当一个核上有离线任务运行的时候,在线任务调度到了其对应的 HT 上时,离线任务会被驱赶走。听起来离线混得很惨对不对?但是这就是我们保证 HT 资源不被争抢的机制。

SMT expeller 特性是基于 Group Identity 框架进一步实现了超线程 (HT) 隔离调度,保障高优先级业务不会受到来自 HT 的低优先级任务干扰。通过 Group Identity 框架进一步实现的超线程调度隔离,可以很好保障高优先级业务不会受到来自对应 HT 上的低优先级任务的干扰。

处理器硬件资源管理技术

我们的内核架构支持 Intel®Resource Director Technology(Intel®RDT),它是一种处理器支持的硬件资源管理技术。包括监控 Cache 资源的 Cache Monitoring Technology (CMT) ,监控内存带宽的 Memory Bandwidth Monitoring (MBM),负责 Cache 资源分配的 Cache Allocation Technology(CAT) 和监控内存带宽的 Memory Bandwidth Allocation(MBA)。

其中,CAT 使得 LLC(Last Level Cache) 变成了一种支持 QualityofService(QoS) 的资源。在混部环境里面,如果没有 LLC 的隔离,离线应用不停的读写数据导致大量的 LLC 占用,会导致在线的 LLC 被不断污染,影响数据访问甚至硬件中断延迟升高、性能下降。

MBA 用于内存带宽分配。对于内存带宽敏感的业务来说,内存带宽比 LLC 控制更能影响性能和时延。在混部环境里面,离线通常是资源消耗型的,特别是一些 AI 类型的作业对内存带宽资源的消耗非常大,内存占用带宽一旦达到瓶颈,可能使得在线业务的性能和时延成倍的下降,并表现出 CPU 水位上升。

Memcg 后台回收

在原生的内核系统中,当容器的内存使用量达到上限时,如果再申请使用内存,则当前的进程上下文中就会进行直接内存回收的动作,这无疑会影响当前进程的执行效率,引发性能问题。那我们是否有方法当容器的内存达到一定水线的时候让其提前进行内存的异步回收?这样就有比较大的概率避免容器内的进程在申请使用内存时由于内存使用达到上限而进入直接内存回收。

我们知道在内核中有一个 kswapd 的后台内核线程,用来当系统的内存使用量达到一定水位以后来进行异步的内存回收。但是这里有一种情况,比如当前高优业务容器的内存使用已经达到一个比较紧张的状态,但是宿主机总体的空闲内存还有很多,这样内核的 kswapd 的线程就不会被唤醒进行内存回收,导致这些内存使用压力大的高优容器的内存没有机会被回收。这是个比较大的矛盾。由于目前原生内核中没有 memory Cgroup 级别的内存异步回收机制,也就是说容器的内存回收严重依赖宿主机层面的 kswapd 的回收或者只能依靠自己的同步回收,这会严重影响一些高优容器的业务性能。

基于以上背景,阿里云操作系统团队提供了一个类似宿主机层面的 kswapd 的基于 Memcg 的异步回收策略,可以根据用户需求提前进行容器级别的内存回收机制,做到提前内存释压。

具体的异步回收过程可以通过下面这幅图进行描述:

Memcg 全局水位线分级

通常资源消耗型的离线任务时常会瞬间申请大量的内存,使得系统的空闲内存触及全局 min 水线,引发系统所有任务进入直接内存回收的慢速流程,这时时延敏感型的在线业务很容易发生性能抖动。此场景下,无论是全局 kswapd 后台回收还是 Memcg 级别的后台回收机制,都是无能为力的。

我们基于 “内存消耗型的离线任务通常对时延不敏感” 这样一个事实,设计了 “Memcg的全局 min 水线分级功能” 来解决上述抖动难题。在标准 upstream 全局共享 min 水线的基础上,将离线任务的全局 min 水线上移让其提前进入直接内存回收,同时将时延敏感的在线任务的全局 min 水线下移,在一定程度上实现了离线任务和在线任务的 min 水线隔离。这样当离线任务瞬间大量内存申请的时候,会将离线任务抑制在其上移的 min 水线,避免了在线任务发生直接内存回收,随后当全局 kswapd 回收一定量的内存后,离线任务的短时间抑制得以解除。

核心思想是通过为在离线容器设置不同标准的全局水位线来分别控制其申请内存的动作,这样能让离线容器的任务在申请内存时先与在线业务进入直接内存回收,解决离线容器瞬间申请大量内存引发的问题。

对 Linux 内存管理有一定基础的同学也可以查阅下面这幅图,详细记录了在离线容器混部过程中多种水位线作用下的走势:

Memcg OOM 优先级

在真实的业务场景中,特别是内存超卖环境,当发生 Global OOM 的时候,有理由去选择杀掉那些优先级比较低的离线业务,而保护高优先级在线业务;当发生离线 Memcg OOM 的时候,有理由去选择杀掉那些优先级比较低的作业,而保高优先级离线作业。这其实是云原生场景中一个比较通用的需求,但是目前的标准 Linux 内核并不具备这个能力。在选择杀进程的时候,内核会有一个算法去选择 victim,但通常是找一个 OOM score 最大的进程杀,这个被杀的进程有可能是在线高优业务进程,这并不是我们想看到的。

基于以上原因,阿里云操作系统团队提供了一个 Memcg OOM 优先级的特性,通过该特性我们可以保障在系统由于内存紧张而发生 OOM 时通过选择低优的业务进程进行 Kill,从而避免高优业务进程被杀的可能,可以大幅降低由于在线业务进程退出而给客户业务带来的影响。

CgroupV1 Writeback 限流

Block IO Cgroup 自合入内核之后,一直存在一个问题,就是只能对 Direct IO 进行限流 (buffer IO 之后短期内执行 fsync 也可以限流),因为这些 IO 到达 Block Throttle 层时,当前进程就是真正发起 IO 的进程,根据进程可以获取到相应的 Cgroup 从而正确记账,如果超过了用户设置的带宽 /IOPS 上限,则进行限制。对于那些 buffer 写,且最终由 kworker 线程下发的 IO,Block Throttle 层无法通过当前进程获取 IO 所属的 Cgroup,也就无法对这些 IO 进行限流。

基于以上背景,目前在 Cgroup V2 版本中已经支持异步 IO 限流,但是在 Cgroup V1 中并不支持,由于目前在云原生环境下主要还是使用 Cgroup V1 版本,阿里云操作系统团队通过建立 Page <-> Memcg <-> blkcg 这三者的关系实现了在 Cgroup V1 中对 IO 异步限流的功能,限流的主要算法基本上和 Cgroup V2 保持一致。

blk-iocost 权重控制

正常情况下,为了避免一个 IO 饥饿型作业轻易耗尽整个系统 IO 资源,我们会设置每个 Cgroup 的 IO 带宽上限,其最大缺点是即使设备空闲,配置上限的 Cgroup 在其已发送 IO 超过上限时不能继续发送 IO,引起存储资源浪费。

基于以上需求,出现了一种 IO 控制器 - IOCOST,该控制器是基于 blkcg 的权重来分配磁盘的资源,可以做到在满足业务 IO QOS 的前提下,尽最大程度利用磁盘的 IO 资源,一旦出现磁盘 IO 能力达到上限导致触及 QOS 设置的目标时,此时 iocost 控制器会通过权重来控制各个 group 的 IO 使用,在此基础上,blk-iocost 又拥有一定的自适应能力,尽可能的避免磁盘能力被浪费。

展望与期待

以上所有这些的资源隔离能力目前已经完全贡献给了龙蜥社区,相关源码可以参考ANCK(Anolis Cloud Kernel),有兴趣的同学可以关注龙蜥社区:openanolis.cn/

同时,阿里云容器服务团队也正在与操作系统团队合作,通过阿里云容器服务 ACK 敏捷版及 CNStack(CloudNative Stack) 产品家族对外输出,持续落地 ACK Anywhere,为更多企业赋能。在商用化版本里,我们将完全基于云原生社区标准,以插件化的方式无缝的安装在 K8s 集群为输出形态线下交付客户。其中核心的 OS 层隔离能力,已经发布到支持多架构的开源、中立、开放的 Linux 操作系统发行版-龙蜥(Anolis OS)中。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

通俗理解 Kubernetes 中的服务,搞懂后真“有趣”

发表于 2021-11-22

1. 复习 pod 相关核心结构

1.1 pod 结构

pod 相当于一个容器,pod 有独立的 ip 地址,也有自己的 hostname,利用 namespace 进行资源隔离,相当于一个独立沙箱环境。

pod 内部封装的是容器,可以封装一个,或者多个容器(通常是一组相关的容器)

1.2 pod 网络

pod 有自己独立的 IP 地址

pod 内部的容器之间是通过 localhost 进行访问

2. pod 如何对外提供访问

首先 pod 有自己的 IP 和 hostname,但 pod 是虚拟的资源对象 (在计算机中表现为进程),没有对应实体 (物理机,物理网卡) 与之对应,所以是无法直接对外提供服务访问的。

因此如果 pod 想对外提供服务,必须绑定物理机端口 (即在物理机上开启端口,让这个端口和 pod 的端口进行映射),这样就可以通过物理机进行数据包的转发。

下面以一台 Linux 系统的机器为例子( logstash 是做日志收集用的)

)

3. pod 的负载均衡

很关键的一个问题:一组相关的 pod 副本,如何实现访问负载均衡?就如当请求达到,请求转发给哪个 pod 比较好?

一个想法就是用 pod 再部署一个 Nginx。

举例:如下图,注意下图右边的 Node 里面有两个是 支付 服务,与订单服务的是不同类型的 pod。如果一个请求订单的服务发来上面那个 Nginx,那这个 pod 可以有 4 条转发路线,可以想到用 hash 呀什么的把不同请求映射到不同的 pod 去转发。但能不能这么做呢?

)

思考:pod 是一个进程,是有生命周期的,一旦宕机、版本更新都会创建新的 pod( IP 地址会变化,hostname 会变化),此时再使用 Nginx 做负载均衡不太合适,因为它不知道 pod 发生了改变,那请求就不能被接受了。所以服务发生了变化它根本不知道,Nginx 无法发现服务,不能用 Nginx 做负载均衡。那该如何实现呢?使用 Service 资源对象。

3.1 什么是 Service 资源对象

POD IP:pod 的 IP 地址

NODE IP:物理机的 IP 地址

cluster IP:虚拟 IP,是由 kubernetes 抽象出的 service 对象,这个 service 对象就是一个 VIP (virtual IP, VIP) 的资源对象

3.2 service 如何实现负载均衡

例如现在要负载均衡地访问一组相同的服务副本——订单,这时就要去做一个 service,对外表现出是一个进程或资源对象,有虚拟的 IP (VIP) 和端口。请求会访问 service,然后 service 自己会 负载均衡 地发送给相应服务的 POD,也就是下图中 4 个相同的 pod。

)

3.3 深入 service VIP

service 和 pod 都是一个进程,都是虚拟的,因此实际上 service 也不能对外网提供服务

service 和 pod 之间可以直接进行通信,它们的通信属于局域网通信

负载策略:把请求交给 service 后,service 使用 iptables,ipvs 来实现数据包的分发

而要对外网提供服务,首先需要和之前一样 在物理机上也绑定一个端口 来接受访问请求,然后把请求转发给 service,service 再把数据包分发给相应的 POD。访问流程如下图所示:

)

思考1:那 service 对象是如何和 pod 进行关联的呢?

它们之间的关联利用的 还是标签选择器 selector。且service 只能对 一组相同的副本 提供服务,不能跨组提供服务。如果有另一组,需要再创建一个 service。因此不同的业务会有不同的 service。

举例:service 和 一组 pod 副本是通过标签选择器进行关联的,相同的副本的标签是一样的。

selector:app = x 选择一组订单的服务的 pod,创建一个 service;app = y 选择了一组支付的服务的 pod。通过一个 endpoints 属性存储这组 pod 的 IP 地址,这样就有了映射关系了 (关联起来)。

)

思考2:pod 宕机或发布新版本了,service 是如何发现 pod 已经发生变化的?

通过 k8s 中的一个组件 —— kube-proxy (第 1 篇有提到过),每个 NODE 里都运行着这个服务。它需要做的工作如下图右侧:

)

service 实现服务的发现:kube-proxy 监控 pod,一旦发现 pod 服务变化,将会把新的 ip 地址更新到 service。

注意:endpoints 那些都是存储在 etcd 里的 (也是第 1 篇提到过的),所以 kube-proxy 更新的存储在 etcd 里的映射关系。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis分布式锁应用(实现+原理)

发表于 2021-11-22

在分布式系统中,当不同进程或线程一起访问共享资源时,会造成资源争抢,如果不加以控制的话,就会引发程序错乱。此时使用分布式锁能够非常有效的解决这个问题,它采用了一种互斥机制来防止线程或进程间相互干扰,从而保证了数据的一致性。

Redis分布式锁介绍

分布式锁并非是 Redis 独有,比如 MySQL 关系型数据库,以及 Zookeeper 分布式服务应用,它们都实现分布式锁,只不过 Redis 是基于缓存实现的。

Redis 分布式锁有很对应用场景,举个简单的例子,比如春运时,您需要在 12306 上抢购回家火车票,但 Redis 数据库中只剩一张票了,此时有多个用户来预订购买,那么这张票会被谁抢走呢?Redis 服务器又是如何处理这种情景的呢?在这个过程中就需要使用分布式锁。

Redis 分布式锁主要有以下特点:

  • 第一:互斥性是分布式锁的重要特点,在任意时刻,只有一个线程能够持有锁;
  • 第二:锁的超时时间,一个线程在持锁期间挂掉了而没主动释放锁,此时通过超时时间来保证该线程在超时后可以释放锁,这样其他线程才可以继续获取锁;
  • 第三:加锁和解锁必须是由同一个线程来设置;
  • 第四:Redis 是缓存型数据库,拥有很高的性能,因此加锁和释放锁开销较小,并且能够很轻易地实现分布式锁。

Redis分布式锁命令

分布式锁的本质其实就是要在 Redis 里面占一个“坑”,当别的进程也要来占时,发现已经有人蹲了,就只好放弃或者稍做等待。这个“坑”同一时刻只允许被一个客户端占据,也就是本着“先来先占”的原则。

1) 常用命令

Redis 分布式锁常用命令如下所示:

  • SETNX key val:仅当key不存在时,设置一个 key 为 value 的字符串,返回1;若 key 存在,设置失败,返回 0;
  • Expire key timeout:为 key 设置一个超时时间,以 second 秒为单位,超过这个时间锁会自动释放,避免死锁;
  • DEL key:删除 key。

上述 SETNX 命令相当于占“坑”操作,EXPIRE 是为避免出现意外用来设置锁的过期时间,也就是说到了指定的过期时间,该客户端必须让出锁,让其他客户端去持有。

但还有一种情况,如果在 SETNX 和 EXPIRE 之间服务器进程突然挂掉,也就是还未设置过期时间,这样就会导致 EXPIRE 执行不了,因此还是会造成“死锁”的问题。为了避免这个问题,Redis 作者在 2.6.12 版本后,对 SET 命令参数做了扩展,使它可以同时执行 SETNX 和 EXPIRE 命令,从而解决了死锁的问题。

直接使用 SET 命令实现,语法格式如下:

1
css复制代码SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
  • EX second:设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond:设置键的过期时间为毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecondvalue 。
  • NX:只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX:只在键已经存在时,才对键进行设置操作。

2) 命令应用

下面进行简单的命令演示:

1
2
3
4
5
6
7
8
9
10
ruby复制代码127.0.0.1:6379> SETNX WEBNAME www.biancheng.net
(integer) 1
127.0.0.1:6379> EXPIRE WEBNAME 60
(integer) 1
127.0.0.1:6379> GET WEBNAME
"www.biancheng.net"
127.0.0.1:6379> TTL WEBNAME
(integer) 33
127.0.0.1:6379> SET name www.biancheng.net EX 60 NX
OK

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文详解 Kubernetes 中的服务发现

发表于 2021-11-22

K8S 服务发现之旅

Kubernetes 服务发现是一个经常让我产生困惑的主题之一。本文分为两个部分:

网络方面的背景知识

深入了解 Kubernetes 服务发现

要了解服务发现,首先要了解背后的网络知识。这部分内容相对浅显,如果读者熟知这一部分,完全可以跳过,直接阅读服务发现部分。

开始之前还有一个需要提醒的事情就是,为了详细描述这一过程,本文略长。

K8S 网络基础

要开始服务发现的探索之前,需要理解以下内容:

Kubernetes 应用运行在容器之中,容器处于 Pod 之内。

每个 Pod 都会附着在同一个大的扁平的 IP 网络之中,被称为 Pod 网络(通常是 VXLAN 叠加网络)。

每个 Pod 都有自己的唯一的 IP 地址,这个 IP 地址在 Pod 网络中是可路由的。

)

上述三个因素结合起来,让每个应用(应用的组件和服务)无需通过 NAT 之类的网络过程,就能够直接通信。

动态网络

在对应用进行横向扩容时,会在 Pod 网络中加入新的 Pod,新 Pod 自然也伴随着新的 IP 地址;如果对应用进行缩容,旧的 Pod 及其 IP 会被删除。这个过程看起来很是混乱。

应用的滚动更新和撤回也存在同样的情形——加入新版本的新 Pod,或者移除旧版本的旧 Pod。新 Pod 会加入新 IP 到 Pod 网络中,被终结的旧 Pod 会删除其现存 IP。

如果没有其它因素,每个应用服务都需要对网络进行监控,并管理一个健康 Pod 的列表。这个过程会非常痛苦,另外在每个应用中编写这个逻辑也是很低效的。幸运的是,Kubernetes 用一个对象完成了这个过程——Service。

把这个对象叫做 Service 是个坏主意,我们已经用这个单词来形容应用的进程或组件了。

还有一个值得注意的事情:Kubernetes 执行 IP 地址管理(IPAM)职责,对 Pod 网络上已使用和可用的 IP 地址进行跟踪。

Service 带来稳定性

Kubernetes Service 对象在一组提供服务的 Pod 之前创建一个稳定的网络端点,并为这些 Pod 进行负载分配。

一般会在一组完成同样工作的 Pod 之前放置一个 Service 对象。例如可以在你的 Web 前端 Pod 前方提供一个 Service,在认证服务 Pod 之前提供另一个。行使不同职责的 Pod 之前就不应该用单一的 Service 了。

客户端和 Service 通信,Service 负责把流量负载均衡给 Pod。

)

在上图中,底部的 Pod 会因为伸缩、更新、故障等情况发生变化,而 Service 会对这些变化进行跟踪。同时 Service 的名字、IP 和端口都不会发生变化。

K8S Service 解析

可以把 Kubernetes Service 理解为前端和后端两部分:

前端:名称、IP 和端口等不变的部分。

后端:符合特定标签选择条件的 Pod 集合。

前端是稳定可靠的,它的名称、IP 和端口在 Service 的整个生命周期中都不会改变。前端的稳定性意味着无需担心客户端 DNS 缓存超时等问题。

后端是高度动态的,其中包括一组符合标签选择条件的 Pod,会通过负载均衡的方式进行访问。

)

这里的负载均衡是一个简单的 4 层轮询。它工作在连接层面,所以同一个连接里发起的所有请求都会进入同一个 Pod。因为在 4 层工作,所以对于 7 层的 HTTP 头或者 Cookie 之类的东西是无法感知的。

小结

应用在容器中运行,在 Kubernetes 中体现为 Pod 的形式。Kubernetes 集群中的所有 Pod 都处于同一个平面的 Pod 网络,有自己的 IP 地址。这意味着所有的 Pod 之间都能直接连接。然而 Pod 是不稳定的,可能因为各种因素创建和销毁。

Kubernetes 提供了稳定的网络端点,称为 Service,这个对象处于一组相似的 Pod 前方,提供了稳定的名称、IP 和端口。客户端连接到 Service,Service 把流量负载均衡给 Pod。

接下来聊聊服务发现。

深入 K8S 服务发现

深入了解 Kubernetes 服务发现

服务发现实际上包含两个功能点:

服务注册

服务发现

服务注册

服务注册过程指的是在服务注册表中登记一个服务,以便让其它服务发现。

)

Kubernetes 使用 DNS 作为服务注册表。

为了满足这一需要,每个 Kubernetes 集群都会在 kube-system 命名空间中用 Pod 的形式运行一个 DNS 服务,通常称之为集群 DNS。

每个 Kubernetes 服务都会自动注册到集群 DNS 之中。

注册过程大致如下:

向 API Server 用 POST 方式提交一个新的 Service 定义;

这个请求需要经过认证、鉴权以及其它的准入策略检查过程之后才会放行;

Service 得到一个 ClusterIP(虚拟 IP 地址),并保存到集群数据仓库;

在集群范围内传播 Service 配置;

集群 DNS 服务得知该 Service 的创建,据此创建必要的 DNS A 记录。

上面过程中,第 5 个步骤是关键环节。集群 DNS 使用的是 CoreDNS,以 Kubernetes 原生应用的形式运行。

CoreDNS 实现了一个控制器,会对 API Server 进行监听,一旦发现有新建的 Service 对象,就创建一个从 Service 名称映射到 ClusterIP 的域名记录。这样 Service 就不必自行向 DNS 进行注册,CoreDNS 控制器会关注新创建的 Service 对象,并实现后续的 DNS 过程。

DNS 中注册的名称就是 metadata.name,而 ClusterIP 则由 Kubernetes 自行分配。

)

Service 对象注册到集群 DNS 之中后,就能够被运行在集群中的其它 Pod 发现了。

Endpoint 对象

Service 的前端创建成功并注册到服务注册表(DNS)之后,剩下的就是后端的工作了。后端包含一个 Pod 列表,Service 对象会把流量分发给这些 Pod。

毫无疑问,这个 Pod 列表需要是最新的。

Service 对象有一个 Label Selector 字段,这个字段是一个标签列表,符合列表条件的 Pod 就会被服务纳入到服务的负载均衡范围之中。参见下图:

)

Kubernetes 自动为每个 Service 创建 Endpoints 对象。Endpoints 对象的职责就是保存一个符合 Service 标签选择器标准的 Pod 列表,这些 Pod 将接收来自 Service 的流量。

下面的图中,Service 会选择两个 Pod,并且还展示了 Service 的 Endpoints 对象,这个对象里包含了两个符合 Service 选择标准的 Pod 的 IP。

在后面我们将解释网络如何把 ClusterIP 流量转发给 Pod IP 的过程,还会引用到 Endpoints 对象。

服务发现

假设我们在一个 Kubernetes 集群中有两个应用,my-app 和 your-app,my-app 的 Pod 的前端是一个 名为 my-app-svc 的 Service 对象;your-app Pod 之前的 Service 就是 your-app-svc。

这两个 Service 对象对应的 DNS 记录是:

my-app-svc:10.0.0.10

your-app-svc:10.0.0.20

)

要使用服务发现功能,每个 Pod 都需要知道集群 DNS 的位置才能使用它。因此每个 Pod 中的每个容器的 /etc/resolv.conf 文件都被配置为使用集群 DNS 进行解析。

如果 my-app 中的 Pod 想要连接到 your-app 中的 Pod,就得向 DNS 服务器发起对域名 your-app-svc 的查询。假设它们本地的 DNS 解析缓存中没有这个记录,则需要把查询提交到集群 DNS 服务器。会得到 you-app-svc 的 ClusterIP(VIP)。

这里有个前提就是 my-app 需要知道目标服务的名称。

至此,my-app 中的 Pod 得到了一个目标 IP 地址,然而这只是个虚拟 IP,在转入目标 Pod 之前,还有些网络工作要做。

网络

一个 Pod 得到了 Service 的 ClusterIP 之后,就尝试向这个 IP 发送流量。然而 ClusterIP 所在的网络被称为 Service Network,这个网络有点特别——没有路由指向它。

因为没有路由,所有容器把发现这种地址的流量都发送到了缺省网关(名为 CBR0 的网桥)。这些流量会被转发给 Pod 所在节点的网卡上。节点的网络栈也同样没有路由能到达 Service Network,所以只能发送到自己的缺省网关。路由到节点缺省网关的数据包会通过 Node 内核——这里有了变化。

回顾一下前面的内容。首先 Service 对象的配置是全集群范围有效的,另外还会再次说到 Endpoints 对象。我们要在回顾中发现他们各自在这一过程中的职责。

每个 Kubernetes 节点上都会运行一个叫做 kube-proxy 的系统服务。这是一个基于 Pod 运行的 Kubernetes 原生应用,它所实现的控制器会监控 API Server 上 Service 的变化,并据此创建 iptables 或者 IPVS 规则,这些规则告知节点,捕获目标为 Service 网络的报文,并转发给 Pod IP。

有趣的是,kube-proxy 并不是一个普遍意义上的代理。它的工作不过是创建和管理 iptables/IPVS 规则。这个命名的原因是它过去使用 unserspace 模式的代理。

每个新 Service 对象的配置,其中包含它的 ClusterIP 以及 Endpoints 对象(其中包含健康 Pod 的列表),都会被发送给 每个节点上的 kube-proxy 进程。

kube-proxy 会创建 iptables 或者 IPVS 规则,告知节点捕获目标为 Service ClusterIP 的流量,并根据 Endpoints 对象的内容转发给对应的 Pod。

也就是说每次节点内核处理到目标为 Service 网络的数据包时,都会对数据包的 Header 进行改写,把目标 IP 改为 Service Endpoints 对象中的健康 Pod 的 IP。

原本使用的 iptables 正在被 IPVS 取代(Kubernetes 1.11 进入稳定期)。长话短说,iptables 是一个包过滤器,并非为负载均衡设计的。IPVS 是一个 4 层的负载均衡器,其性能和实现方式都比 iptables 更适合这种使用场景。

总结

需要消化的内容很多,简单回顾一下。

创建新的 Service 对象时,会得到一个虚拟 IP,被称为 ClusterIP。服务名及其 ClusterIP 被自动注册到集群 DNS 中,并且会创建相关的 Endpoints 对象用于保存符合标签条件的健康 Pod 的列表,Service 对象会向列表中的 Pod 转发流量。

与此同时集群中所有节点都会配置相应的 iptables/IPVS 规则,监听目标为 ClusterIP 的流量并转发给真实的 Pod IP。这个过程如下图所示:

)

一个 Pod 需要用 Service 连接其它 Pod。首先向集群 DNS 发出查询,把 Service 名称解析为 ClusterIP,然后把流量发送给位于 Service 网络的 ClusterIP 上。

然而没有到 Service 网络的路由,所以 Pod 把流量发送给它的缺省网关。这一行为导致流量被转发给 Pod 所在节点的网卡,然后是节点的缺省网关。这个操作中,节点的内核修改了数据包 Header 中的目标 IP,使其转向健康的 Pod。

)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…237238239…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%