Semaphore(信号量)

信号量 信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量 简单的说就是通过获取资源和释放资源来进行同步的一种策略 用法 用法一共有以下几步: 创建信号量 获取信号量 释放信号量 //1.创建信号量为10 sem = semaphore.NewWeighted(10) for i := 0; i <100; i++ { go func() { ctx := context.TODO() //2.获取一个信号量, 信号量一共10个,获取最多获取10,超过的gorutine会挂起 if err := sem.Acquire(ctx, 1); err != nil { doSomething() } //3. 释放信号量,1个 sem.Release(1) }() } 代码解读 Weighted 结构(NewWeighted 返回的数据结构) type Weighted struct { size int64 //总大小,就是NewWeighted传入的个数 cur int64 //当前消耗的个数 mu sync.Mutex //互斥锁 waiters list.List //等待列表, 当信号量不足时等待的列表 } waiter 结构(等待列表保存的结构) type waiter struct { n int64 //需要的资源数 ready chan<- struct{} // 用来通知gorutine } Acquire 方法 func (s *Weighted) Acquire(ctx context.Context, n int64) error { s.mu.Lock() //判断当前资源是否足够,如果足够则累加s.cur直接返回 if s.size-s.cur >= n && s.waiters.Len() == 0 { s.cur += n s.mu.Unlock() return nil } //如果n 大于总资源就会等待超时或取消,(如果这种情况使用的是context.TODO,或者Background, Done返回nil,就会一直等待,导致gorutine泄露) if n > s.size { s.mu.Unlock() <-ctx.Done() return ctx.Err() } //这里就是判断,总资源够,但是剩余资源不够的情况, 这时就需要其他获取资源的groutine释放资源 ready := make(chan struct{}) w := waiter{n: n, ready: ready} elem := s.waiters.PushBack(w) //把当前等待的放入队列 s.mu.Unlock() //等待超时或者资源充足 select { case <-ctx.Done(): //等待超时 err := ctx.Err() s.mu.Lock() select { case <-ready: //如果超时后立刻有足够资源时也会返回 err = nil default: //超时后的逻辑 isFront := s.waiters.Front() == elem //判断当前等待的是队列头部元素(因为资源被释放后优先分配给队列头部的) s.waiters.Remove(elem) //移除超时的waiter if isFront && s.size > s.cur { //去掉头部后并且有剩余就看下,下个waiter能否满足 s.notifyWaiters() } } s.mu.Unlock() return err case <-ready: //等待资源 return nil } } notifyWaiters 方法 func (s *Weighted) notifyWaiters() { for { //取出队列头部的waiter next := s.waiters.Front() if next == nil { //没有等待的waiter就退出循环 break } w := next.Value.(waiter) //如果资源不能满足则退出循环 if s.size-s.cur < w.n { break } //如果能满足,就移除当前元素,关闭waiter.ready(Acquire 中 <-w.ready 就能取消阻塞) s.cur += w.n s.waiters.Remove(next) close(w.ready) } } Release 方法 func (s *Weighted) Release(n int64) { s.mu.Lock() //从当前使用减去n s.cur -= n if s.cur < 0 { //如果Release的资源超过,Acquire的资源就会发生panic s.mu.Unlock() panic("semaphore: released more than held") } //唤醒waiter s.notifyWaiters() s.mu.Unlock() }

2022-07-01 23:51:45    |    2 分钟    |    262 字    |    Fengbin

K3s安装

安装步骤 根据官方文档使用 curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh - 一条命令安装 安装过程的坑 建议使用Raspberry OS, 之前使用过Ubuntu 22.04 安装后发生节点有时Ready有时NotReady反复横跳, 最后用Raspberry OS安装成功 Raspberry也有点小坑,需要在/boot/cmdline.txt文件最后用空格,不要换行, 添加cgroup_memory=1 cgroup_enable=memory, 然后重启 `` 官方文档

2022-06-29 00:02:05    |    1 分钟    |    24 字    |    Fengbin

链路追踪

链路追踪是什么 链路追踪是在分布式条件下将一个请求还原成一个完整调用链条,可以分析调用拓扑,延迟分析,性能分析. 链路追踪的好处 分析网络,服务耗时(通过链路追踪事件可以知道网络延迟,服务延迟) 分析网络拓扑(链路分析) 故障定位(配合日志,进行故障定位) 原理 Trace Trace代表一个调用链路,通过TraceID来标记, 一次请求调用的各个服务TraceID在全局都是唯一的 Span Span代表一个调用范围拥有(ParentID, SpanID), ParentID代表他的调用者SpanID, SpanID代表本层次调用id 通过TraceID标记一个完整调用链都调用了哪些调用过程, 通过ParendID,和SpanID还原了调用的父子关系 Annotation 通过上面三个ID只能还原调用关系, 还不能进行性能分析和定位,所以还要添加一些辅助的注解信息, 可以同定义事件比如: Client Send: 客户端调用开始 Client Receive: 客户端调用结束 Server Send: 服务端发送 Server Receive: 服务端接收 图一是一个调用关系图,展示了TraceID, SpanID, ParentID 之间的关系,和传递 其中,个方框是一个服务,箭头代表调用关系,一个完整调用链中trace是相同的, 每个服务有各自的SpanID, ParentID是调用方的SpanID, 通过这些ID我们可以知道调用的上下级关系 图二是通过Annotation附带信息进行性能分析,通过Client Send 到 Server Receive可以分析出请求服务的网络时延;通过Server Receive到Server Send可以分析出调用时延;同理Server Send到Client Receive分析出响应的网络时延, Client Send到Client Receive整个请求的时延

2022-06-27 14:24:23    |    1 分钟    |    53 字    |    Fengbin

Once

once是什么 和singlefight有些相似,singlefight是并发执行时只有一个在执行, once也是并发时只有一个在执行,只不过,只执行一次,再次调用不会在执行 once怎么用 var A int var once = sync.Once{} func initA() int { once.Do(func() { //这里只会执行一次 A = 10 //A=10 只会执行一次,并且所有并发进来的,都需要等待A=10 完成后返回 }) return A // A=10 happens before 读取A, 所以initA()在所有gorutine里,都返回10 } 这个例子我们可以构造一个懒汉模式单例 源码阅读 Once结构 type Once struct { done uint32 m Mutex } Once结构很简单,只有两个字段, done来表示是否执行完成, m为互斥锁 Do函数 func (o *Once) Do(f func()) { //判断done 如果没完成,则执行doSlow函数,否则直接返回退出函数 if atomic.LoadUint32(&o.done) == 0 { o.doSlow(f) } } doSlow (第一次并发执行时才会进入的分支) func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() //上锁 if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) //完成标志设置为1 f() //执行传入的函数 } }

2022-06-25 20:50:16    |    1 分钟    |    91 字    |    Fengbin

数据竞赛(Data Race)

没有安全的数据竞赛,不要使用各种炫技的无锁方式等骚操作! 什么是数据竞赛 并行程序在未使用同步方法(atomic, lock)的情况下, 并发读写共享资源就会造成数据竞争 数据竞赛检测原理 通过编译器注入检测代码, 检测代码会保存读写内存的 线程id, 时钟, 读写位置和长度, 是否写入等信息, 在运行的过程判断,读写内存是否有交叉,是否满足happens-before等条件,来判断数据竞赛 如何避免 使用同步方法去解决happens-before 比如使用atomic包, sync包 或者使用chan go检测数据竞赛方法 使用go工具链 go build -race 编译一个带有数据竞赛检测的可执行程序,会在编译期插入代码,这种程序消耗内存和CPU是不带检测的数倍到数十倍,不可大范围用于生产环境 go run -race 直接运行一个带检测的程序(内部也经过编译) go test -race 运行待检测的单元测试 go install -race 编译并安装一个待检测的可执行程序 注意数据竞赛检测是需要程序运行到有竞赛的代码才会检测到!运行不到的是不会检测出来的!

2022-06-23 16:39:21    |    1 分钟    |    37 字    |    Fengbin

go内存模型

介绍 Go内存模型指定了一种条件,在这种条件下,可以保证读取一个goroutine中的变量,以观察不同goroutine中写入同一变量所产生的值。 建议 修改多个goroutine同时访问的数据必须序列化访问 序列化访问保护数据使用channel操作或者其他同步原语比如sync或者sync/atomic包 Happens before 在一个goroutine中,读写必须表现得就像它们按照程序指定的顺序执行一样;也就是说 在一个goroutine中,处理器和编译器可以重排读写的执行顺序,仅当重排后的行为不改变语言的设定.因为重排,一个goroutine观察到的执行顺序可能与另一个goroutine观察到的顺序不同.举个例子,如果一个goroutine执行a = 1; b = 2;,另一个可能会观察到b在a之前更新. 为了指定读写的需要,我们定义了在Go程序中执行内存操作的偏序(partial order)。如果事件$e_1$先发生于事件$e_2$我们说$e_2$后发生于$e_1$. 同样的如果$e_1$没有先发生于$e_2$并且$e_1$没有后发生于$e_2$那么我们说$e_1 e_2$同时发生. 在单个gorutine里, happens-before的顺序是程序表示的顺序. 如果下面两个条件成立,则允许变量v的读r 观察到对v的写w: r没有先发生于w 没有其他对v的写w’ ,后发生于w, 先发生于r (也就是在w 和 r之间不存在 w') 为了保证r能读到w的写,要确保w是允许r观察的唯一写入.也就是说,如果以下两个条件均成立,则保证r观察到w: w先发生于r 任何其他写入共享变量v都要先发生于w或者后发生于r 这对条件比第一对更严格,这一对要求没有其他的写同时发生于w或r 在单个gorutine中没有并发,这两个定义是等价的:读r观察最近写入w到v的值. 在多个gorutine访问一个共享变量v,必须使用同步事件来建立happens-before条件来确保读到期望的写. 变量v的类型为零值时,变量v的初始化行为就像在内存模型中写入一样。(初始化等同于写入) 读写超过机器字(machine word)的行为就和以未指定的顺序执行多个机器字大小的操作一样(超过machine word 每个machine word 读取和写入顺序可能不是期望的) 总之, 想要在一个gorutine中读到另一个gorutine的写就要保证, 写在读之前发生,如果我们不加控制,这个写先于读的顺序就很难保证,所以我们需要使用atomic或者和lock机制来保证顺序保证happens-before 同步 初始化 程序初始化在单个goroutine中运行,但该goroutine可能会创建其他并发运行的goroutine。 如果包p导入包q,则q的init函数的完成时间在任何p的开始之前。 函数main的开始。main在所有init函数完成后发生。 Goroutine创建 启动新goroutine的go语句发生在goroutine开始执行之前。 举例 var a string func f() { print(a) } func hello() { a = "hello, world" go f() } 调用hello将在将来hello, world,可能hello已经返回 Goroutine销毁 goroutine的退出不能保证先发生于任何事件,例如: ...

2022-06-23 10:33:29    |    2 分钟    |    319 字    |    Fengbin

false sharing

false sharing我们一般说的是多核的问题,这个问题主要出现在CPU的缓存上,我们知道CPU有多级缓存,而CPU缓存单单位是行(主流一个缓存行是64Byte, 也就是8个int64) CPU加载缓存 当我们要操作一个变量A时会把A附近的64个字节都加载到缓存行中(空间局部性原理),这样在CPU的缓存里操作时要比在内存中要快 多核CPU缓存问题 在多核心中每个CPU核心都有自己的缓存,如果A和B变量是挨着的, 当CPU1要写A变量, CPU2读变量B, 因为AB挨着,CPU1和CPU2都把它们加载到自己的缓存中,并且AB在同一个缓存行中,CPU1改了A导致了CPU2中B缓存失效,CPUB就得重新从内存中加载缓存 这种情况就是"假共享"/“伪共享”/“false sharing” 说白了就是,CPU各自都有一份,因为邻近变量修改导致了其他核心缓存失效 例子 CASE1 type FS struct { X int64 Y int64 } func share() { var a FS wg := sync.WaitGroup{} wg.Add(2) start := time.Now() go func() { for i := 0; i < 100000000; i++ { a.X++ } wg.Done() }() go func() { for i := 0; i < 100000000; i++ { a.Y++ } wg.Done() }() wg.Wait() fmt.Println(time.Since(start)) } 这种就创造了两个变量挨着在同一缓存行的情况,这个代码在我本机运行时间为305.462012ms CASE 2 我们把上面结构体改一下,X和Y间隔56个字节, 使他们不在一个缓存行, 其他部分不变 type FS struct { X int64 _ [7]int64 Y int64 } 本地运行时间变为201.209402ms, 节省了100ms 注意区分false sharing和data race false sharing讲的是多个核心数据共享缓存失效的问题, 只是影响程序性能,并不影响程序正确性(解决这个问题可以使用内存填充,就像CASE2那样) data race讲的是并发访问同一个变量问题, 会导致意外的情况, 程序不能正确执行(没有安全的数据竞争,发生数据竞争要使用同步原语解决)

2022-06-22 15:04:38    |    1 分钟    |    101 字    |    Fengbin

利特尔法则(等候理论,排队理论)

定义 在一个稳定的系统中,长期的平均顾客人数(L),等于长期的有效抵达率(λ),乘以顾客在这个系统中平均的等待时间(W); 或者,我们可以用一个代数式来表达: $L=λW$ 用白话说的话就是,在W时间内最多排多少人,能够让最后一个人也能在W时间内完成服务,也就是第一个人恰好出去,最后一个人恰好进来,这样最后一个人也能在W时间内出去 案例 就是说L的最后一名也可以在W时间内完成服务,按图上例子来说,就是4分钟内能同时服务多少个顾客 因为顾客的进入速度是2, 所以4分钟内最多也就是8个 如果进如速度是10那么4分钟就是40个 类比服务请求 如果一个请求的响应时长是1s, 系统的QPS是10/s, 那么系统同时处理请求的最佳个数是 10/s * 1s = 10个 如果系统里同时请求数超过10个,那么就会造成响应时间延长

2022-06-21 22:44:01    |    1 分钟    |    20 字    |    Fengbin

指数加权平均(EWA)

EWA是什么 EWA是以指数式递减加权的移动平均, 是一种近似平均(也可以理解为一段时间的平均值,因为越久的数据对当前的影响越小,小到一定程度就可以忽略,可以理解为一段时间的平均值) 基本公式 $V_t=βV_{t-1} + (1-β)R_t$ $V_t$ 代表t时刻的平均值 $βV_{t-1}$代表t-1时刻的平均值 $R_t$ 是t时刻的真实值 $β$ 范围在0-1之间 平均天数为 $N=\frac {1} {1-β}$ $β=0.5$则平均个数N=2 $β=0.9$则平均个数N=$\frac{1}{1-0.9}=10$ 也就是平均最近10次的 可以做什么 计算$\frac {1} {1-β}$个数据的平均值,减少噪声影响,平滑数据 好处比其他平均的好处是 不需要保存最近N次的数据,只需要保存上次计算的平均值

2022-06-20 17:44:00    |    1 分钟    |    27 字    |    Fengbin

hugo 支持github评论

先在utteranc的configuration部分找到安装utteranc app到仓库,选择一个仓库并安装 在页面Enable Utterances部分找到js代码 在自己用的主题上找到关于comment的layout,把js代码添加到里面 运行

2022-06-20 15:02:41    |    1 分钟    |    6 字    |    Fengbin