计算机网络简要概述

计算机网络分层 市面上对网络分层主要是有三种分层 七层 五层 四层 应用层 应用层 应用层 表示层 会话层 传输层 传输层 传输层 网络层 网络层 网络层 数据链路层 数据链路层 网络接口层 物理层 物理层 每层的职责 物理层 提供物理介质,电压信号等功能 数据链路层 提供P2P传输 (点对点的, 比如一个路由器到另一个路由器) 网络层 提供E2E传输 (Endpoint to Endpoint,两个端点的传输,中间可能经过若干个路由器,注意区别P2P, E2E > P2P) 传输控制层 提供进程到进程的传输(端口到端口的传输) 应用层 应用自定义个协议 每一层都是通过下层对上层提供接口的形式来提供服务 常用网络设备 交换机 交换机工作在数据链路层, 通过mac地址进行转发, 全双工网络设备, 可以隔离碰撞域, 减少链路上的信号碰撞,提高链路网络利用率 路由器 路由器工作在网际层, 通过ip进行转发, 全双工网络设备, 可以隔离广播域(广播不能通过路由器)

2022-07-04 21:10:39    |    1 分钟    |    53 字    |    Fengbin

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