数据竞争和竞态条件

Go并发中有两个重要的概念:数据竞争(data race)和竞争条件(race condition)。在并发程序中,竞争问题可能是程序面临的最难也是最不容易发现的错误之一。

当有两个或多个协程同时访问同一个内存地址,并且至少有一个是写时,就会发生数据竞争,它造成的影响就是读取变量的值将变得不可知。

数据竞争产生的原因是对于同一个变量的访问不是原子性的。

避免数据竞争可以使用以下三种方式:

  • 使用原子操作
  • 使用mutex对同一区域进行互斥操作
  • 使用管道 (channel) 进行通信以保证仅且只有一个协程在进行写操作

相比于数据竞争,竞争条件也称为资源竞争,受各协程的执行顺序和时机的影响,程序的运行结果产生变化。

竞态条件产生的原因很多是对于同一个资源的一系列连续操作并不是原子性的,也就是说有可能在执行的中途被其他线程抢占,同时这个“其他线程”刚好也要访问这个资源。解决方法通常是:将这一系列操作作为一个critical section(临界区)

i := 0
mutex := sync.Mutex{}
go func() {
mutex.Lock()
defer mutex.Unlock()
i = 1
}() go func() {
mutex.Lock()
defer mutex.Unlock()
i = 2
}()

这里虽然使用了锁来避免了数据竞争,但输出结果仍然是不可控的,因为变量的结果依赖于协程执行的顺序。这就是竞争条件。

乐观锁和悲观锁

概念

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁

    乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人没有修改数据,执行更新操作。如果数据已经被更新过了,根据不同的实现方式执行不同的操作:重试(重新读取更新然后比较)或报异常。

  • 悲观锁

    悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

应用场景:

当竞争不激烈时,即出现并发冲突的概率小。乐观锁更有优势,因为悲观锁加锁和释放锁的操作需要耗费额外的资源;当竞争激烈的时候,悲观锁有优势,因为乐观锁在执行失败的时候需要不断重试,浪费CPU资源。针对这个问题的一个思路是引入退出机制,如果重试次数超过一定阈值后失败推出。当然,应该避免在高竞争环境下使用乐观锁。

实现方式

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。

乐观锁的实现方式主要有两种:CAS机制和版本号机制,

  • CAS

    CAS机制就是Compare And Swap。他的操作逻辑是:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。即CAS在更新之前先比较一下,然后决定是否要更新。

    这里的比较和更新是两个操作,其原子性是由CPU支持的,在硬件层面上进行保证。

    CAS有个缺点,就是ABA问题:

    假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

    (1)线程1读取内存中数据为A;

    (2)线程2将该数据修改为B;

    (3)线程2将该数据修改为A;

    (4)线程1对数据进行CAS操作

    在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

    在AtomicInteger的例子中,ABA似乎没有什么危害。但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,虽然栈顶不变,但是栈的结构可能已发生了变化。

    对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。

  • 版本号机制

    版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

与悲观锁相比,乐观锁功能有很多限制,比如CAS操作只能保证单个变量的原子性。

kubernetes中的乐观并发

更新资源时

k8s中的资源都有一个metadata.ResourceVersion字段,当api-server执行update操作的时候通常会先执行get操作,server会比较该字段,如果相同则更新成功,并修改该字段,如果不同,则更新失败。

Leader Election

在kubernetes中,通常kube-scheduler和kube-controller-manager都是多副本进行部署来保证高可用的,这里利用的就是leader election机制。

leader election 指的是一个程序为了高可用会有多个副本,但是每一个时候只允许一个进程在工作。k8s基于乐观并发控制实现了leader election,使得一些控制面组件有多个副本,但只有一个副本在工作,从而达到高可用。

$ kubectl get ep -n kube-system kube-scheduler -o yaml
apiVersion: v1
kind: Endpoints
metadata:
annotations:
control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"kube-master-1_ad5220de-2442-11e9-91f6-52540025e0cf","leaseDurationSeconds":15,"acquireTime":"2019-02-22T02:09:15Z","renewTime":"2019-03-14T07:37:19Z","leaderTransitions":1}'
name: kube-scheduler
namespace: kube-system

holderIdentity:表示当前那个副本在工作

leaseDurationSeconds:租期的时间

acquireTime:获得锁的时间

renewTime:刷新锁的时间

leaderTransitions:leader 交换的次数

实现原理

其原理就是利用kubernetes中的configmap、endpoints、lease这三种锁资源实现了一个分布式锁。推荐使用Lease,因为Lease 对象本身就是用来协调租约对象的,其Spec 定义与Leader 选举机制需要操控的属性是一致的。使用Configmap 和Endpoints 对象更多是为了向后兼容,伴随着一定的负面影响。以Endpoints 为例,Leader 每隔固定周期就要续约,这使得Endpoints 对象处于不断的变化中。Endpoints 对象会被每个节点的kube-proxy 等监听,任何Endpoints 对象的变更都会推送给所有节点的kube-proxy,这为集群引入了不必要的网络流量。

  1. 大致逻辑就是多个副本会同时更新某个资源annotation中的holderIdentity字段,写入字段值的操作被称为获取锁资源。由于k8s时乐观并发,只有一个会更新成功,更新成功的这个副本就会成为leader。
  2. 在 leader 被选举成功之后,leader 为了保住自己的位置,需要定时去更新这个 Lease 资源的状态,即一个时间戳信息,表明自己有在一直工作没有出现故障,这一操作称为续约。
  3. 其他 candidate 也不是完全闲着,而是也会定期尝试获取这个资源,检查资源的信息,时间戳有没有太久没更新,否则认为原来的 leader 故障失联无法正常工作,并更新此资源的 holder 为自己,成为 leader 开始工作并同时定期续约。

使用举例

代码路径:client-go/examples/leader-election/main.go

		// leader election uses the Kubernetes API by writing to a
// lock object, which can be a LeaseLock object (preferred),
// a ConfigMap, or an Endpoints (deprecated) object.
// Conflicting writes are detected and each client handles those actions
// independently.
config, err := buildConfig(kubeconfig)
if err != nil {
klog.Fatal(err)
}
client := clientset.NewForConfigOrDie(config) run := func(ctx context.Context) {
// complete your controller loop here
klog.Info("Controller loop...") select {}
} // use a Go context so we can tell the leaderelection code when we
// want to step down
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // listen for interrupts or the Linux SIGTERM signal and cancel
// our context, which the leader election code will observe and
// step down
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() {
<-ch
klog.Info("Received termination, signaling shutdown")
cancel()
}() // we use the Lease lock type since edits to Leases are less common
// and fewer objects in the cluster watch "all Leases".
// 指定锁的资源对象,这里使用了Lease资源,还支持configmap,endpoint,或者multilock(即多种配合使用)
lock := &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: leaseLockName,
Namespace: leaseLockNamespace,
},
Client: client.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: id,
},
} // start the leader election code loop
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: lock,
// IMPORTANT: you MUST ensure that any code you have that
// is protected by the lease must terminate **before**
// you call cancel. Otherwise, you could have a background
// loop still running and another process could
// get elected before your background loop finished, violating
// the stated goal of the lease.
ReleaseOnCancel: true,
LeaseDuration: 60 * time.Second,//租约时间
RenewDeadline: 15 * time.Second,//更新租约的
RetryPeriod: 5 * time.Second,//非leader节点重试时间
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
//变为leader执行的业务代码
// we're notified when we start - this is where you would
// usually put your code
run(ctx)
},
OnStoppedLeading: func() {
// 进程退出
// we can do cleanup here
klog.Infof("leader lost: %s", id)
os.Exit(0)
},
OnNewLeader: func(identity string) {
//当产生新的leader后执行的方法
// we're notified when new leader elected
if identity == id {
// I just got the lock
return
}
klog.Infof("new leader elected: %s", identity)
},
},
})

乐观锁和悲观锁在kubernetes中的应用的更多相关文章

  1. [转]MySQL中乐观锁、悲观锁(共享锁、排他锁)简介

    InnoDB与MyISAM Mysql 在5.5之前默认使用 MyISAM 存储引擎,之后使用 InnoDB. MyISAM 操作数据都是使用的表锁,你更新一条记录就要锁整个表,导致性能较低,并发不高 ...

  2. web开发中的两把锁之数据库锁:(高并发--乐观锁、悲观锁)

    这篇文章讲了 1.同步异步概念(消去很多疑惑),同步就是一件事一件事的做:sychronized就是保证线程一个一个的执行. 2.我们需要明白,锁机制有两个层面,一种是代码层次上的,如Java中的同步 ...

  3. mysql中的乐观锁和悲观锁

    mysql中的乐观锁和悲观锁的简介以及如何简单运用. 关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的. mysql的悲观锁: 其实理解起来非常简单,当数据被外界修改持保守态度,包括 ...

  4. 老司机带大家领略MySQL中的乐观锁和悲观锁

    原文地址:https://cloud.tencent.com/developer/news/227982 为什么需要锁 在并发环境下,如果多个客户端访问同一条数据,此时就会产生数据不一致的问题,如何解 ...

  5. Java中的锁之乐观锁与悲观锁

    1.  分类一:乐观锁与悲观锁 a)悲观锁:认为其他线程会干扰本身线程操作,所以加锁 i.具体表现形式:synchronized关键字和lock实现类 b)乐观锁:认为没有其他线程会影响本身线程操作, ...

  6. 利用MySQL中的乐观锁和悲观锁实现分布式锁

    背景 对于一些并发量不是很高的场景,使用MySQL的乐观锁实现会比较精简且巧妙. 下面就一个小例子,针对不加锁.乐观锁以及悲观锁这三种方式来实现. 主要是一个用户表,它有一个年龄的字段,然后并发地对其 ...

  7. Hibernate事务与并发问题处理(乐观锁与悲观锁)

    目录 一.数据库事务的定义 二.数据库事务并发可能带来的问题 三.数据库事务隔离级别 四.使用Hibernate设置数据库隔离级别 五.使用悲观锁解决事务并发问题 六.使用乐观锁解决事务并发问题 Hi ...

  8. mysql的锁--行锁,表锁,乐观锁,悲观锁

    一 引言--为什么mysql提供了锁 最近看到了mysql有行锁和表锁两个概念,越想越疑惑.为什么mysql要提供锁机制,而且这种机制不是一个摆设,还有很多人在用.在现代数据库里几乎有事务机制,aci ...

  9. Hibernate乐观锁、悲观锁和多态

     乐观锁和悲观锁 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁 ...

随机推荐

  1. Flutter和iOS混编详解

    前言 下面的内容是最近在使用Flutter和我们自己项目进行混编时候的一些总结以及自己踩的一些坑,处理完了就顺便把整个过程以及一些我们可能需要注意的点全都梳理出来,希望对有需要的小伙伴有点帮助,也方便 ...

  2. Yapi Docker 部署

    参考 https://github.com/Ryan-Miao/docker-yapi , 并使用该代码的脚本构建yapi image. 部署mongodb docker run \ --name m ...

  3. 一、深入学习c++先要练好的内功

    掌握进程虚拟地址空间区域的划分 课程讲的内容建立在x86 32位的Linux系统下. 任何的编程语言会产生两种东西:指令和数据.磁盘上的可执行文件在启动时都会加载到内存当中,但是不会加载到物理内存中, ...

  4. 行为参数化与lambda表达式 - 读《Java 8实战》

    零. 概述 第一部分:1~3章 主要讲了行为参数化和Lambda表达式 第二部分:4~7章 主要讲了流的应用,包括流与集合差异,流的操作,收集器,注的并行执行 第三部分:8~12章 主要讲了怎样用Ja ...

  5. 好客租房40-react组件基础综合案例-案例需求分析

    实现 案例的数据 渲染评论列表 有评论 没有评论 暂无评论 获取评论信息 包括评论人和受控组件 发表评论 更新评论 //导入react import React from 'react' import ...

  6. 好客租房29-从jsx中抽离事件处理程序

    从jsx中抽离过多js逻辑代码 会显得非常混乱 推荐:将逻辑抽离到单独的方法中 保证jsx结构清晰 //导入react     import React from 'react'           ...

  7. MongoDB启动报错:Unrecognized option: storage try 'mongod --help' for more information(已解决)

    问题说明: 今天在使用配置文件方式启动MongoDB时,一直启动失败,报错显示:Unrecognized option: storage try 'mongod --help' for more in ...

  8. [THUSCH2017] 杜老师

    description \(T\)次询问,每次问\(L,L+1...R\)有多少种子集满足子集中乘积为完全平方数. solution 50pt 首先双倍经验 通常的思路是:平方数即每个质因子指数为偶 ...

  9. 【leetcode 206】 反转链表(简单)

    链表 概念: 区别于数组,链表中的元素不是存储在内存中连续的一片区域,链表中的数据存储在每一个称之为「结点」复合区域里,在每一个结点除了存储数据以外,还保存了到下一个结点的指针(Pointer). 由 ...

  10. CSS中html的标签元素分类

    在CSS中,html中的标签元素大体被分为三种不同的类型: 块状元素.内联元素(又叫行内元素)和内联块状元素.    常用的块状元素有:  <div>.<p>.<h1&g ...