├── .gitattributes ├── README.md ├── golang ├── 1.图解Go的互斥锁Mutex.md ├── 10.垃圾收集器.md ├── 10.垃圾收集器 │ ├── GC Start.png │ ├── GC Start2-1615730101024.png │ ├── GC Start2.png │ ├── GCTRACE.png │ ├── GCTrace-1615610560550.png │ ├── GcWork-1615715723190.png │ ├── GcWork.png │ ├── MarkWork.png │ ├── TRI-COLOR.png │ ├── TRI-COLOR2.png │ ├── TRI-COLOR3.png │ ├── TRI-COLOR4-5381225.png │ ├── TRI-COLOR4.png │ ├── WorkMark.png │ ├── YuasaWritebarrier-1616425266521.png │ ├── YuasaWritebarrier-1616425298896.png │ ├── YuasaWritebarrier.png │ ├── gcWork2.png │ └── mutator.png ├── 11.Go语言抢占调度.md ├── 12.Go语言中栈内存.md ├── 12.Go语言中栈内存 │ ├── G Stack.png │ ├── Linux_stack-1617529597692.png │ ├── Linux_stack-1617529674577.png │ ├── Linux_stack.png │ ├── Stack layout.png │ ├── function.png │ ├── new_stack.png │ └── stackpool.png ├── 13.Go语言函数对用与接口.md ├── 2.Go中的Pool.md ├── 2.Go中的Pool │ ├── Group 22.png │ ├── Group 25.png │ ├── Group 26.png │ ├── Group 39.png │ └── Group 8.png ├── 3.Go中的Channel.md ├── 3.Go中的Channel │ ├── Group 23-1610188386214.png │ ├── Group 23.png │ ├── Group 23.svg │ ├── Group 40.png │ ├── Group 66-1610198781892.svg │ ├── Group 66.svg │ ├── Group 84.svg │ ├── Group 85-1610199680150.svg │ ├── Group 85.svg │ ├── Group100.svg │ ├── ch.svg │ ├── ch_struct.svg │ ├── image-20210109114055217.png │ └── image-20210109153245743.png ├── 4.Go中WaitGroup引发的内存对齐.md ├── 4.Go中WaitGroup引发的内存对齐 │ ├── Group 15.svg │ ├── Group 21.svg │ ├── Group 49.svg │ └── Group 50.svg ├── 6.Go中内存分配.md ├── 7.Go语言时间轮的实现.md ├── 8.Go语言调度器.md ├── 9.Go中定时器实现原理及源码解析.md ├── Go中内存分配 │ ├── Group 37.png │ ├── Large Object Allocation.png │ ├── allocCache-1611497692285.png │ ├── allocCache.png │ ├── mcache.png │ ├── mcentral-1747885.png │ ├── mcentral.png │ ├── mchache2.png │ ├── mheap-1747918.png │ └── mheap.png ├── Go中定时器实现原理(未完) │ ├── new_timer.png │ └── timer1.13.png ├── Go中的网络轮询器.md ├── Go中的网络轮询器 │ ├── Accept.png │ ├── listen.png │ ├── multiplexing model.png │ ├── netFD.png │ └── pollCache.png ├── Go语言函数对用与接口 │ ├── Linux_stack-1617529674577.png │ ├── call stack-8316529.png │ ├── call stack-8318004.png │ ├── call stack.png │ ├── call stack10-1618750698509.png │ ├── call stack10-1618753456961.png │ ├── call stack10.png │ ├── call stack2.png │ ├── call stack3.png │ ├── call stack4.png │ ├── call stack5.png │ ├── call stack6-1618668525016.png │ ├── call stack6.png │ ├── call stack7.png │ ├── call stack8-1618749781455.png │ ├── call stack8.png │ ├── call stack9-1618749857402.png │ ├── call stack9.png │ ├── function.png │ └── image-20210412204906209.png ├── Go语言实现布谷鸟过滤器.md ├── Go语言实现布谷鸟过滤器 │ ├── Cuckoo Filter Insert.png │ ├── Cuckoo Filter Insert2.png │ ├── Cuckoo Filter Insert3.png │ ├── Cuckoo Filter Insert4.png │ ├── CuckooFilter.png │ └── position.png ├── Go语言抢占调度 │ ├── image-20210327132559064.png │ ├── image-20210327132807943.png │ ├── image-20210327144615922.png │ ├── image-20210327145625294.png │ ├── image-20210327145652092.png │ ├── image-20210327152443777.png │ ├── image-20210327152534498.png │ ├── image-20210327152857867.png │ ├── preempt.png │ ├── stw_preempt.png │ └── sysmon_preempt.png ├── Go语言时间轮的实现 │ ├── Group 37.png │ ├── bucket.png │ ├── taskList.png │ ├── timewheel.png │ ├── timewheelAdd9S.png │ ├── timewheel_start.png │ └── timewheellevel2.png ├── Go语言调度器 │ ├── GMP-3738208.png │ ├── M_bind_CPU-1613744972579.png │ ├── M_bind_CPU.png │ ├── execute.png │ ├── mstart.png │ ├── runq.png │ ├── schedule-1613903287779.png │ └── schedule.png ├── 图解Go的互斥锁 │ ├── Group 1-1607867443891.png │ ├── Group 5.png │ ├── Group 6.png │ └── Group 7.png └── 如何阅读Go源码.md ├── 深入istio ├── 1.深入Istio.md ├── 1.深入Istio │ ├── 80614165_p0_master1200.jpg │ └── image-20201107174326312.png ├── 2.深入Istio.md ├── 2.深入Istio │ └── image-20201115153309906.png ├── 2.深入Istio:Pilot服务发现.md ├── 2.深入Istio:Pilot服务发现 │ └── istio.png ├── 3.深入Istio.md ├── 3.深入Istio │ ├── Group2.png │ ├── Group3.png │ ├── configserver (1).png │ └── configserver.png ├── 4.深入Istio.md ├── 4.深入Istio │ ├── Blank diagram.png │ ├── Group 3.png │ ├── Group 4.png │ ├── Group 5.png │ ├── Group 6.png │ ├── dsServerStart.png │ ├── image-20201130231001341.png │ └── startPush.png ├── 5.深入Istio源码:Pilot-agent.md ├── 5.深入Istio源码:Pilot-agent │ ├── Group 7.png │ └── Group 8.png ├── 从一个例子入手Istio.md ├── 从一个例子入手Istio │ ├── 74617380_p0_master1200.jpg │ ├── arch.svg │ ├── image-20201024215839257.png │ └── image-20201025163623769.png └── 深入Istio │ ├── Group 1-1606017533527.png │ ├── Group 1.png │ ├── Group 2.png │ ├── serviceController.png │ └── serviceController流程图.png └── 深入k8s ├── 1.深入k8s:在k8s中运行第一个程序.md ├── 10.深入k8s:优先级及抢占机制源码分析.md ├── 10.深入k8s:优先级及抢占机制源码分析 ├── 20200905190824.png ├── 20200912223315.png └── 20200912223345.png ├── 11.深入k8s:kubelet工作原理及其初始化源码分析.md ├── 11.深入k8s:kubelet工作原理及其初始化源码分析 ├── 20200920120525.png ├── 20200920120529.png ├── 20200920120534.png ├── 20200920120537.png └── 20200920121011.jpg ├── 12.深入k8s:kubelet创建pod流程源码分析.md ├── 12.深入k8s:kubelet创建pod流程源码分析 ├── 20200920120525.png ├── 20200926201132.jpg ├── 20200926201151.png └── 20200926201155.png ├── 13.深入k8s:Pod水平自动扩缩HPA及其源码分析.md ├── 13.深入k8s:Pod水平自动扩缩HPA及其源码分析 ├── 20201004174855.jpg ├── 20201004174900.png ├── 20201004174910.png └── 20201004174924.png ├── 14.深入k8s:kube-proxy代理ipvs及其源码分析.md ├── 14.深入k8s:kube-proxy代理ipvs及其源码分析 └── 20201008173220.png ├── 15.深入k8s:Event事件处理及其源码分析.md ├── 15.深入k8s:Event事件处理及其源码分析 ├── 20201011223447.jpg ├── 20201011223452.png ├── 20201011223458.png └── 20201011223510.png ├── 16.深入k8s ├── 63831060_p0_master1200.jpg ├── 71001144_p0_master1200.jpg ├── image-20201017000513025.png ├── image-20201017000845410.png ├── image-20201017213554429.png ├── image-20201017213620364.png └── image-20201017233145402.png ├── 16.深入k8s:Informer使用及其源码分析.md ├── 2.深入k8s:Pod对象中重要概念及用法.md ├── 2.深入k8s:Pod对象中重要概念及用法 └── 20200724234654.jpg ├── 3.深入k8s:Deployment控制器.md ├── 3.深入k8s:Deployment控制器 ├── 20200726183429.png ├── 711c07208358208e91fa7803ebc73058.jpg └── 72cc68d82237071898a1d149c8354b26.png ├── 4.深入k8s:容器持久化存储 ├── 01723d5f23d906a801215aa0f293ac.jpg@1280w_1l_2o_100sh.jpg └── pv_and_pvc.png ├── 4.深入k8s:持久卷PV、PVC及其源码分析.md ├── 5.深入k8s:StatefulSet控制器 ├── StatefulSet A.png ├── image-20200807220814361.png └── 发生故障.png ├── 5.深入k8s:StatefulSet控制器及其源码分析.md ├── 6.深入k8s:守护进程DaemonSet及其源码分析.md ├── 6.深入k8s:守护进程DaemonSet及其源码分析 └── 20200809185827.jpg ├── 7.深入k8s:任务调用Job与CronJob及源码分析.md ├── 7.深入k8s:任务调用Job与CronJob及源码分析 └── 20200823163612.png ├── 8.深入k8s:资源控制Qos和eviction及其源码分析.md ├── 8.深入k8s:资源控制Qos和eviction及其源码分析 └── 20200829221848.jpg ├── 9.深入k8s:调度器及其源码分析.md ├── 9.深入k8s:调度器及其源码分析 ├── 20200905191205.jpg ├── scheduler.png └── 调度流程.png ├── Docker容器实现原理.md ├── Docker容器实现原理 ├── 2017-11-30-docker-filesystems.png ├── 8a7b5cfabaab2d877a1d4566961edd5f.png └── 95c957b3c2813bb70eb784b8d1daedc6.png ├── Pod对象 └── 8c016391b4b17923f38547c498e434cf.png ├── Readme.md └── 在k8s中运行第一个程序 └── k8s集群图.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 此仓库会归档所有一些我的技术学习文章。最新的文章会发布在我的博客中:https://www.luozhiyun.com 2 | 3 | 如果觉得不错的话,记得点个start,感谢~ 4 | 5 | ## 深入 Go 语言 6 | 7 | [如何编译调试Go runtime源码](golang/如何阅读Go源码.md) 8 | 9 | [多图详解Go的互斥锁Mutex](golang/1.图解Go的互斥锁Mutex.md) 10 | 11 | [多图详解Go的sync.Pool源码](golang/2.Go中的Pool.md) 12 | 13 | [多图详解Go中的Channel源码](golang/3.Go中的Channel.md) 14 | 15 | [Go中由WaitGroup引发对内存对齐思考](golang/4.Go中WaitGroup引发的内存对齐.md) 16 | 17 | [详解Go中内存分配源码实现](golang/6.Go中内存分配.md) 18 | 19 | [Go语言中时间轮的实现](golang/7.Go语言时间轮的实现.md) 20 | 21 | [详解Go语言调度循环源码实现](golang/8.Go语言调度器.md) 22 | 23 | [Go中定时器实现原理及源码解析](golang/9.Go中定时器实现原理及源码解析.md) 24 | 25 | [Go语言GC实现原理及源码分析](golang/10.垃圾收集器.md) 26 | 27 | [从源码剖析Go语言基于信号式抢占式调度](golang/11.Go语言抢占调度.md) 28 | 29 | [一文教你搞懂 Go 中栈操作](golang/12.Go语言中栈内存.md) 30 | 31 | [从汇编出发深入 Go语言函数调用 ](golang/13.Go语言函数对用与接口.md) 32 | 33 | [Go语言实现布谷鸟过滤器 ](golang/Go语言实现布谷鸟过滤器.md) 34 | 35 | [详解Go语言I/O多路复用netpoller模型](golang/Go中的网络轮询器.md) 36 | 37 | ## 深入 istio 源码分析 38 | 39 | [从一个例子入手Istio](深入istio/从一个例子入手Istio.md) 40 | 41 | [1.深入Istio源码:Sidecar注入实现原理](深入istio/1.深入Istio.md) 42 | 43 | [2.深入Istio源码:Pilot服务发现](深入istio/2.深入Istio源码:Pilot服务发现.md) 44 | 45 | [3.深入Istio源码:Pilot配置规则ConfigController](深入istio/3.深入Istio.md) 46 | 47 | [4.深入Istio源码:Pilot的Discovery Server如何执行xDS异步分发?](深入istio/4.深入Istio.md) 48 | 49 | [5.深入Istio源码:Pilot-agent作用及其源码分析](深入istio/5.深入Istio源码:Pilot-agent.md) 50 | 51 | ## 深入k8s(kubernates)源码分析 52 | 53 | 一开始的时候并没有想过会写这么多,以及会有勇气去翻阅k8s写源码分析,毕竟k8s我们会使用就已经不错了,所以第二篇和第三篇没有加上源码分析,后面再补上。 54 | 55 | [Docker容器实现原理](%E6%B7%B1%E5%85%A5k8s/Docker容器实现原理.md) 56 | 57 | [1.深入k8s:在k8s中运行第一个程序](%E6%B7%B1%E5%85%A5k8s/1.%E6%B7%B1%E5%85%A5k8s%EF%BC%9A%E5%9C%A8k8s%E4%B8%AD%E8%BF%90%E8%A1%8C%E7%AC%AC%E4%B8%80%E4%B8%AA%E7%A8%8B%E5%BA%8F.md) 58 | 59 | [2.深入k8s:Pod对象中重要概念及用法](%E6%B7%B1%E5%85%A5k8s/2.%E6%B7%B1%E5%85%A5k8s%EF%BC%9APod%E5%AF%B9%E8%B1%A1%E4%B8%AD%E9%87%8D%E8%A6%81%E6%A6%82%E5%BF%B5%E5%8F%8A%E7%94%A8%E6%B3%95.md) 60 | 61 | [3.深入k8s:Deployment控制器](%E6%B7%B1%E5%85%A5k8s/3.%E6%B7%B1%E5%85%A5k8s%EF%BC%9ADeployment%E6%8E%A7%E5%88%B6%E5%99%A8.md) 62 | 63 | [4.深入k8s:持久卷PV、PVC及其源码分析](%E6%B7%B1%E5%85%A5k8s/4.%E6%B7%B1%E5%85%A5k8s%EF%BC%9A%E6%8C%81%E4%B9%85%E5%8D%B7PV%E3%80%81PVC%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) 64 | 65 | [5.深入k8s:StatefulSet控制器及其源码分析](%E6%B7%B1%E5%85%A5k8s/5.%E6%B7%B1%E5%85%A5k8s%EF%BC%9AStatefulSet%E6%8E%A7%E5%88%B6%E5%99%A8%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) 66 | 67 | [6.深入k8s:守护进程DaemonSet及其源码分析](%E6%B7%B1%E5%85%A5k8s/6.%E6%B7%B1%E5%85%A5k8s%EF%BC%9A%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8BDaemonSet%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) 68 | 69 | [7.深入k8s:任务调用Job与CronJob及源码分析](%E6%B7%B1%E5%85%A5k8s/7.%E6%B7%B1%E5%85%A5k8s%EF%BC%9A%E4%BB%BB%E5%8A%A1%E8%B0%83%E7%94%A8Job%E4%B8%8ECronJob%E5%8F%8A%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) 70 | 71 | [8.深入k8s:资源控制Qos和eviction及其源码分析](%E6%B7%B1%E5%85%A5k8s/8.%E6%B7%B1%E5%85%A5k8s%EF%BC%9A%E8%B5%84%E6%BA%90%E6%8E%A7%E5%88%B6Qos%E5%92%8Ceviction%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) 72 | 73 | [9.深入k8s:调度器及其源码分析](%E6%B7%B1%E5%85%A5k8s/9.%E6%B7%B1%E5%85%A5k8s%EF%BC%9A%E8%B0%83%E5%BA%A6%E5%99%A8%E5%8F%8A%E5%85%B6%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) 74 | 75 | [10.深入k8s:优先级及抢占机制源码分析](%E6%B7%B1%E5%85%A5k8s/10.深入k8s:优先级及抢占机制源码分析.md) 76 | 77 | [11.深入k8s:kubelet工作原理及其初始化源码分析](%E6%B7%B1%E5%85%A5k8s/11.深入k8s:kubelet工作原理及其初始化源码分析.md) 78 | 79 | [12.深入k8s:kubelet创建pod流程源码分析](%E6%B7%B1%E5%85%A5k8s/12.深入k8s:kubelet创建pod流程源码分析.md) 80 | 81 | [13.深入k8s:Pod水平自动扩缩HPA及其源码分析](%E6%B7%B1%E5%85%A5k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析.md) 82 | 83 | [14.深入k8s:kube-proxy代理ipvs及其源码分析](%E6%B7%B1%E5%85%A5k8s/14.深入k8s:kube-proxy代理ipvs及其源码分析.md) 84 | 85 | [15.深入k8s:Event事件处理及其源码分析](%E6%B7%B1%E5%85%A5k8s/15.深入k8s:Event事件处理及其源码分析.md) 86 | 87 | [16.深入k8s:Informer使用及其源码分析](深入k8s/16.深入k8s:Informer使用及其源码分析.md) 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /golang/1.图解Go的互斥锁Mutex.md: -------------------------------------------------------------------------------- 1 | # 多图详解Go的互斥锁Mutex 2 | 3 | > 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 4 | > 5 | > 本文使用的go的源码时14.4 6 | 7 | ## Mutex介绍 8 | 9 | Mutex 结构体包含两个字段: 10 | 11 | * 字段state:表示当前互斥锁的状态。 12 | 13 | * 字段 sema:是个信号量变量,用来控制等待 goroutine 的阻塞休眠和唤醒。 14 | 15 | ```go 16 | type Mutex struct { 17 | state int32 18 | sema uint32 19 | } 20 | ``` 21 | 22 | 在Go的1.9版本中,为了解决等待中的 goroutine 可能会一直获取不到锁,增加了饥饿模式,让锁变得更公平,不公平的等待时间限制在 1 毫秒。 23 | 24 | state状态字段所表示的含义较为复杂,如下图所示,最低三位分别表示mutexLocked、mutexWoken、mutexStarving,state总共是32位长度,所以剩下的位置,用来表示可以有1<<(32-3)个Goroutine 等待互斥锁的释放: 25 | 26 | ![Group 1](图解Go的互斥锁/Group 1-1607867443891.png) 27 | 28 | 代码表示如下: 29 | 30 | ```go 31 | const ( 32 | mutexLocked = 1 << iota // mutex is locked 33 | mutexWoken 34 | mutexStarving 35 | ) 36 | ``` 37 | 38 | ## 加锁流程 39 | 40 | ### fast path 41 | 42 | ```go 43 | func (m *Mutex) Lock() { 44 | if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 45 | if race.Enabled { 46 | race.Acquire(unsafe.Pointer(m)) 47 | } 48 | return 49 | } 50 | m.lockSlow() 51 | } 52 | ``` 53 | 54 | 加锁的时候,一开始会通过CAS看一下能不能直接获取锁,如果可以的话,那么直接获取锁成功。 55 | 56 | ### lockSlow 57 | 58 | ```go 59 | // 等待时间 60 | var waitStartTime int64 61 | // 饥饿标记 62 | starving := false 63 | // 唤醒标记 64 | awoke := false 65 | // 自旋次数 66 | iter := 0 67 | // 当前的锁的状态 68 | old := m.state 69 | for { 70 | // 锁是非饥饿状态,锁还没被释放,尝试自旋 71 | if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { 72 | if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && 73 | atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { 74 | awoke = true 75 | } 76 | // 自旋 77 | runtime_doSpin() 78 | // 自旋次数加1 79 | iter++ 80 | // 设置当前锁的状态 81 | old = m.state 82 | continue 83 | } 84 | ... 85 | } 86 | ``` 87 | 88 | 进入到lockSlow方法之后首先会判断以下能否可以自旋,判断依据就是通过计算: 89 | 90 | ``` 91 | old&(mutexLocked|mutexStarving) == mutexLocked 92 | ``` 93 | 94 | 可以知道当前锁的状态必须是上锁,并且不能处于饥饿状态,这个判断才为true,然后再看看iter是否满足次数的限制,如果都为true,那么则往下继续。 95 | 96 | 内层if包含了四个判断: 97 | 98 | * 首先判断了awoke是不是唤醒状态; 99 | 100 | * `old&mutexWoken == 0`为真表示没有其他正在唤醒的节点; 101 | 102 | * `old>>mutexWaiterShift != 0`表明当前有正在等待的goroutine; 103 | 104 | * CAS将state的mutexWoken状态位设置为`old|mutexWoken`,即为1是否成功。 105 | 106 | 如果都满足,那么将awoke状态设置为真,然后将自旋次数加一,并重新设置状态。 107 | 108 | 继续往下看: 109 | 110 | ```go 111 | new := old 112 | if old&mutexStarving == 0 { 113 | // 如果当前不是饥饿模式,那么将mutexLocked状态位设置1,表示加锁 114 | new |= mutexLocked 115 | } 116 | if old&(mutexLocked|mutexStarving) != 0 { 117 | // 如果当前被锁定或者处于饥饿模式,则waiter加一,表示等待一个等待计数 118 | new += 1 << mutexWaiterShift 119 | } 120 | // 如果是饥饿状态,并且已经上锁了,那么mutexStarving状态位设置为1,设置为饥饿状态 121 | if starving && old&mutexLocked != 0 { 122 | new |= mutexStarving 123 | } 124 | // awoke为true则表明当前线程在上面自旋的时候,修改mutexWoken状态成功 125 | if awoke { 126 | if new&mutexWoken == 0 { 127 | throw("sync: inconsistent mutex state") 128 | } 129 | // 清除唤醒标志位 130 | new &^= mutexWoken 131 | } 132 | ``` 133 | 134 | 走到这里有两种情况:1. 自旋超过了次数;2. 目前锁没有被持有。 135 | 136 | 所以第一个判断,如果当前加了锁,但是没有处于饥饿状态,也会重复设置`new |= mutexLocked`,即将mutexLocked状态设置为1; 137 | 138 | 如果是old已经是饥饿状态或者已经被上锁了,那么需要设置Waiter加一,表示这个goroutine下面不会获取锁,会等待; 139 | 140 | 如果starving为真,表示当前goroutine是饥饿状态,并且old已经被上锁了,那么设置`new |= mutexStarving`,即将mutexStarving状态位设置为1; 141 | 142 | awoke如果在自旋时设置成功,那么在这里要`new &^= mutexWoken`消除mutexWoken标志位。因为后续流程很有可能当前线程会被挂起,就需要等待其他释放锁的goroutine来唤醒,如果unlock的时候发现mutexWoken的位置不是0,则就不会去唤醒,则该线程就无法再醒来加锁。 143 | 144 | 继续往下: 145 | 146 | ```go 147 | if atomic.CompareAndSwapInt32(&m.state, old, new) { 148 | // 1.如果原来状态没有上锁,也没有饥饿,那么直接返回,表示获取到锁 149 | if old&(mutexLocked|mutexStarving) == 0 { 150 | break // locked the mutex with CAS 151 | } 152 | // 2.到这里是没有获取到锁,判断一下等待时长是否不为0 153 | // 如果不为0,那么加入到队列头部 154 | queueLifo := waitStartTime != 0 155 | // 3.如果等待时间为0,那么初始化等待时间 156 | if waitStartTime == 0 { 157 | waitStartTime = runtime_nanotime() 158 | } 159 | // 4.阻塞等待 160 | runtime_SemacquireMutex(&m.sema, queueLifo, 1) 161 | // 5.唤醒之后检查锁是否应该处于饥饿状态 162 | starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs 163 | old = m.state 164 | // 6.判断是否已经处于饥饿状态 165 | if old&mutexStarving != 0 { 166 | if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { 167 | throw("sync: inconsistent mutex state") 168 | } 169 | // 7.加锁并且将waiter数减1 170 | delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 { 172 | // 8.如果当前goroutine不是饥饿状态,就从饥饿模式切换会正常模式 173 | delta -= mutexStarving 174 | } 175 | // 9.设置状态 176 | atomic.AddInt32(&m.state, delta) 177 | break 178 | } 179 | awoke = true 180 | iter = 0 181 | } else { 182 | old = m.state 183 | } 184 | ``` 185 | 186 | 到这里,首先会CAS设置新的状态,如果设置成功则往下走,否则返回之后循环设置状态。设置成功之后: 187 | 188 | 1. 首先会判断old状态,如果没有饥饿,也没有获取到锁,那么直接返回,因为这种情况在进入到这段代码之前会将new状态设置为mutexLocked,表示已经获取到锁。这里还判断了一下old状态不能为饥饿状态,否则也不能获取到锁; 189 | 2. 判断waitStartTime是否已经初始化过了,如果是新的goroutine来抢占锁,那么queueLifo会返回false;如果不是新的goroutine来抢占锁,那么加入到等待队列头部,这样等待最久的 goroutine 优先能够获取到锁; 190 | 3. 如果等待时间为0,那么初始化等待时间; 191 | 4. 阻塞等待,当前goroutine进行休眠; 192 | 5. 唤醒之后检查锁是否应该处于饥饿状态,并设置starving变量值; 193 | 6. 判断是否已经处于饥饿状态,如果不处于饥饿状态,那么这里直接进入到下一个for循环中获取锁; 194 | 7. 加锁并且将waiter数减1,这里我看了一会,没用懂什么意思,其实需要分两步来理解,相当于state+mutexLocked,然后state再将waiter部分的数减一; 195 | 8. 如果当前goroutine不是饥饿状态或者waiter只有一个,就从饥饿模式切换会正常模式; 196 | 9. 设置状态; 197 | 198 | 下面用图例来解释: 199 | 200 | 这部分的图解是休眠前的操作,休眠前会根据old的状态来判断能不能直接获取到锁,如果old状态没有上锁,也没有饥饿,那么直接break返回,因为这种情况会在CAS中设置加上锁; 201 | 202 | 接着往下判断,waitStartTime是否等于0,如果不等于,说明不是第一次来了,而是被唤醒后来到这里,那么就不能直接放到队尾再休眠了,而是要放到队首,防止长时间抢不到锁; 203 | 204 | ![Group 5](图解Go的互斥锁/Group 5.png) 205 | 206 | 下面这张图是处于唤醒后的示意图,如何被唤醒的可以直接到跳到解锁部分看完再回来。 207 | 208 | 被唤醒一开始是需要判断一下当前的starving状态以及等待的时间如果超过了1ms,那么会将starving设置为true; 209 | 210 | 接下来会有一个if判断, 这里有个细节,因为是被唤醒的,所以判断前需要重新获取一下锁,如果当前不是饥饿模式,那么会直接返回,然后重新进入到for循环中; 211 | 212 | 如果当前是处于饥饿模式,那么会计算一下delta为加锁,并且当前的goroutine是可以直接抢占锁的,所以需要将waiter减一,如果starving不为饥饿,或者等待时间没有超过1ms,或者waiter只有一个了,那么还需要将delta减去mutexStarving,表示退出饥饿模式; 213 | 214 | 最后通过AddInt32将state加上delta,这里之所以可以直接加上,因为这时候state的mutexLocked值肯定为0,并且mutexStarving位肯定为1,并且在获取锁之前至少还有当前一个goroutine在等待队列中,所以waiter可以直接减1。 215 | 216 | ![Group 6](图解Go的互斥锁/Group 6.png) 217 | 218 | ## 解锁流程 219 | 220 | ### fast path 221 | 222 | ```go 223 | func (m *Mutex) Unlock() { 224 | if race.Enabled { 225 | _ = m.state 226 | race.Release(unsafe.Pointer(m)) 227 | } 228 | //返回一个state被减后的值 229 | new := atomic.AddInt32(&m.state, -mutexLocked) 230 | if new != 0 { 231 | //如果返回的state值不为0,那么进入到unlockSlow中 232 | m.unlockSlow(new) 233 | } 234 | } 235 | ``` 236 | 237 | 这里主要就是AddInt32重新设置state的mutexLocked位为0,然后判断新的state值是否不为0,不为0则调用unlockSlow方法。 238 | 239 | ### unlockSlow 240 | 241 | ![Group 7](图解Go的互斥锁/Group 7.png) 242 | 243 | unlockSlow方法里面也分为正常模式和饥饿模式下的解锁: 244 | 245 | ```go 246 | func (m *Mutex) unlockSlow(new int32) { 247 | if (new+mutexLocked)&mutexLocked == 0 { 248 | throw("sync: unlock of unlocked mutex") 249 | } 250 | // 正常模式 251 | if new&mutexStarving == 0 { 252 | old := new 253 | for { 254 | // 如果没有 waiter,或者已经有在处理的情况,直接返回 255 | if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { 256 | return 257 | } 258 | // waiter 数减 1,mutexWoken 标志设置上,通过 CAS 更新 state 的值 259 | new = (old - 1< 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 4 | > 5 | > 本文使用的go的源码时14.4 6 | 7 | WaitGroup使用大家都会,但是其中是怎么实现的我们也需要知道,这样才能在项目中尽可能的避免由于不正确的使用引发的panic。并且本文也将写一下内存对齐方面做一个解析,喜欢大家喜欢。 8 | 9 | ## WaitGroup介绍 10 | 11 | WaitGroup 提供了三个方法: 12 | 13 | ```go 14 | func (wg *WaitGroup) Add(delta int) 15 | func (wg *WaitGroup) Done() 16 | func (wg *WaitGroup) Wait() 17 | ``` 18 | 19 | * Add,用来设置 WaitGroup 的计数值; 20 | * Done,用来将 WaitGroup 的计数值减 1,其实就是调用了 Add(-1); 21 | * Wait,调用这个方法的 goroutine 会一直阻塞,直到 WaitGroup 的计数值变为 0。 22 | 23 | 例子我就不举了,网上是很多的,下面我们直接进入正题。 24 | 25 | ## 解析 26 | 27 | ```go 28 | type noCopy struct{} 29 | 30 | type WaitGroup struct { 31 | // 避免复制使用的一个技巧,可以告诉vet工具违反了复制使用的规则 32 | noCopy noCopy 33 | // 一个复合值,用来表示waiter数、计数值、信号量 34 | state1 [3]uint32 35 | } 36 | // 获取state的地址和信号量的地址 37 | func (wg *WaitGroup) state() (statep *uint64, semap *uint32) { 38 | if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 { 39 | // 如果地址是64bit对齐的,数组前两个元素做state,后一个元素做信号量 40 | return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2] 41 | } else { 42 | // 如果地址是32bit对齐的,数组后两个元素用来做state,它可以用来做64bit的原子操作,第一个元素32bit用来做信号量 43 | return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0] 44 | } 45 | } 46 | ``` 47 | 48 | 这里刚开始,WaitGroup就秀了一把肌肉,让我们看看大牛是怎么写代码的,思考一个原子操作在不同架构平台上是怎么操作的,在看state方法里面为什么要这么做之前,我们先来看看内存对齐。 49 | 50 | ### 内存对齐 51 | 52 | 在维基百科https://en.wikipedia.org/wiki/Data_structure_alignment上我们可以看到对于内存对齐的定义: 53 | 54 | > A memory address *a* is said to be *n-byte aligned* when *a* is a multiple of *n* [bytes](https://en.wikipedia.org/wiki/Byte) (where *n* is a power of 2). 55 | 56 | 简而言之,现在的CPU访问内存的时候是一次性访问多个bytes,比如32位架构一次访问4bytes,该处理器只能从地址为4的倍数的内存开始读取数据,所以要求数据在存放的时候首地址的值是4的倍数存放,者就是所谓的内存对齐。 57 | 58 | 由于找不到Go语言的对齐规则,我对照了一下C语言的内存对齐的规则,可以和Go语言匹配的上,所以先参照下面的规则。 59 | 60 | 内存对齐遵循下面三个原则: 61 | 62 | 1. 结构体变量的**起始地址**能够被其最宽的成员大小整除; 63 | 2. 结构体每个成员相对于**起始地址的偏移**能够被其**自身大小整除**,如果不能则在**前一个成员后面**补充字节; 64 | 3. 结构体总体大小能够**被最宽的成员的大小**整除,如不能则在**后面**补充字节; 65 | 66 | 通过下面的例子来实操一下内存对齐: 67 | 68 | 在32位架构中,int8占1byte,int32占4bytes,int16占2bytes。 69 | 70 | ```go 71 | type A struct { 72 | a int8 73 | b int32 74 | c int16 75 | } 76 | 77 | type B struct { 78 | a int8 79 | c int16 80 | b int32 81 | } 82 | 83 | func main() { 84 | 85 | fmt.Printf("arrange fields to reduce size:\n"+ 86 | "A align: %d, size: %d\n" , 87 | unsafe.Alignof(A{}), unsafe.Sizeof(A{}) ) 88 | 89 | fmt.Printf("arrange fields to reduce size:\n"+ 90 | "B align: %d, size: %d\n" , 91 | unsafe.Alignof(B{}), unsafe.Sizeof(B{}) ) 92 | } 93 | 94 | //output: 95 | //arrange fields to reduce size: 96 | //A align: 4, size: 12 97 | //arrange fields to reduce size: 98 | //B align: 4, size: 8 99 | ``` 100 | 101 | 下面以在32位的架构中运行为例子: 102 | 103 | 在32位架构的系统中默认的对齐大小是4bytes。 104 | 105 | 假设结构体A中a的起始地址为0x0000,能够被最宽的数据成员大小4bytes(int32)整除,所以从0x0000开始存放占用一个字节即0x0000~0x0001;b是int32,占4bytes,所以要满足条件2,需要在a后面padding3个byte,从0x0004开始;c是int16,占2bytes故从0x0008开始占用两个字节,即0x0008~0x0009;此时整个结构体占用的空间是0x0000~0x0009占用10个字节,10%4 != 0, 不满足第三个原则,所以需要在后面补充两个字节,即最后内存对齐后占用的空间是0x0000~0x000B,一共12个字节。 106 | 107 | Group 49 108 | 109 | 同理,相比结构体B则要紧凑些: 110 | 111 | Group 50 112 | 113 | ### WaitGroup中state方法的内存对齐 114 | 115 | 在讲之前需要注意的是noCopy是一个空的结构体,大小为0,不需要做内存对齐,所以大家在看的时候可以忽略这个字段。 116 | 117 | 在WaitGroup里面,使用了uint32的数组来构造state1字段,然后根据系统的位数的不同构造不同的返回值,下面我面先来说说怎么通过sate1这个字段构建waiter数、计数值、信号量的。 118 | 119 | 首先`unsafe.Pointer`来获取state1的地址值然后转换成uintptr类型的,然后判断一下这个地址值是否能被8整除,这里通过地址 mod 8的方式来判断地址是否是64位对齐。 120 | 121 | 因为有内存对齐的存在,在64位架构里面WaitGroup结构体state1起始的位置肯定是64位对齐的,所以在64位架构上用state1前两个元素并成uint64来表示statep,state1最后一个元素表示semap; 122 | 123 | 那么64位架构上面获取state1的时候能不能第一个元素表示semap,后两个元素拼成64位返回呢? 124 | 125 | 答案自然是不可以,因为uint32的对齐保证是4bytes,64位架构中一次性处理事务的一个固定长度是8bytes,如果用state1的后两个元素表示一个64位字的字段的话CPU需要读取内存两次,不能保证原子性。 126 | 127 | 但是在32位架构里面,一个字长是4bytes,要操作64位的数据分布在**两个数据块**中,需要两次操作才能完成访问。如果两次操作中间有可能别其他操作修改,不能保证原子性。 128 | 129 | 同理32位架构想要原子性的操作8bytes,需要由调用方保证其数据地址是64位对齐的,否则原子访问会有异常,我们在这里https://golang.org/pkg/sync/atomic/#pkg-note-BUG可以看到描述: 130 | 131 | > On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned. 132 | 133 | 所以为了保证64位字对齐,只能让变量或开辟的结构体、数组和切片值中的第一个64位字可以被认为是64位字对齐。但是在使用WaitGroup的时候会有嵌套的情况,不能保证总是让WaitGroup存在于结构体的第一个字段上,所以我们需要增加填充使它能对齐64位字。 134 | 135 | 在32位架构中,WaitGroup在初始化的时候,分配内存地址的时候是随机的,所以WaitGroup结构体state1起始的位置不一定是64位对齐,可能会是:`uintptr(unsafe.Pointer(&wg.state1))%8 = 4`,如果出现这样的情况,那么就需要用state1的第一个元素做padding,用state1的后两个元素合并成uint64来表示statep。 136 | 137 | #### 小结 138 | 139 | 这里小结一下,因为为了完成上面的这篇内容实在是查阅了很多资料,才得出这样的结果。所以这里小结一下,在64位架构中,CPU每次操作的字长都是8bytes,编译器会自动帮我们把结构体的第一个字段的地址初始化成64位对齐的,所以64位架构上用state1前两个元素并成uint64来表示statep,state1最后一个元素表示semap; 140 | 141 | 然后在32位架构中,在初始化WaitGroup的时候,编译器只能保证32位对齐,不能保证64位对齐,所以通过`uintptr(unsafe.Pointer(&wg.state1))%8`判断是否等于0来看state1内存地址是否是64位对齐,如果是,那么也和64位架构一样,用state1前两个元素并成uint64来表示statep,state1最后一个元素表示semap,否则用state1的第一个元素做padding,用state1的后两个元素合并成uint64来表示statep。 142 | 143 | 如果我说错了,欢迎来diss我,我觉得我需要学习的地方还有很多。 144 | 145 | Group 21 146 | 147 | ### Add 方法 148 | 149 | ```go 150 | func (wg *WaitGroup) Add(delta int) { 151 | // 获取状态值 152 | statep, semap := wg.state() 153 | ... 154 | // 高32bit是计数值v,所以把delta左移32,增加到计数上 155 | state := atomic.AddUint64(statep, uint64(delta)<<32) 156 | // 获取计数器的值 157 | v := int32(state >> 32) 158 | // 获取waiter的值 159 | w := uint32(state) 160 | ... 161 | // 任务计数器不能为负数 162 | if v < 0 { 163 | panic("sync: negative WaitGroup counter") 164 | } 165 | // wait不等于0说明已经执行了Wait,此时不容许Add 166 | if w != 0 && delta > 0 && v == int32(delta) { 167 | panic("sync: WaitGroup misuse: Add called concurrently with Wait") 168 | } 169 | // 计数器的值大于或者没有waiter在等待,直接返回 170 | if v > 0 || w == 0 { 171 | return 172 | } 173 | if *statep != state { 174 | panic("sync: WaitGroup misuse: Add called concurrently with Wait") 175 | } 176 | // 此时,counter一定等于0,而waiter一定大于0 177 | // 先把counter置为0,再释放waiter个数的信号量 178 | *statep = 0 179 | for ; w != 0; w-- { 180 | //释放信号量,执行一次释放一个,唤醒一个等待者 181 | runtime_Semrelease(semap, false, 0) 182 | } 183 | } 184 | ``` 185 | 186 | 1. add方法首先会调用state方法获取statep、semap的值。statep是一个uint64类型的值,高32位用来记录add方法传入的delta值之和;低32位用来表示调用wait方法等待的goroutine的数量,也就是waiter的数量。如下: 187 | 188 | Group 15 189 | 190 | 2. add方法会调用`atomic.AddUint64`方法将传入的delta左移32位,也就是将counter加上delta的值; 191 | 192 | 3. 因为计数器counter可能为负数,所以int32来获取计数器的值,waiter不可能为负数,所以使用uint32来获取; 193 | 4. 接下来就是一系列的校验,v不能小于零表示任务计数器不能为负数,否则会panic;w不等于,并且v的值等于delta表示wait方法先于add方法执行,此时也会panic,因为waitgroup不允许调用了Wait方法后还调用add方法; 194 | 5. v大于零或者w等于零直接返回,说明这个时候不需要释放waiter,所以直接返回; 195 | 6. ` *statep != state`到了这个校验这里,状态只能是waiter大于零并且counter为零。当waiter大于零的时候是不允许再调用add方法,counter为零的时候也不能调用wait方法,所以这里使用state的值和内存的地址值进行比较,查看是否调用了add或者wait导致state变动,如果有就是非法调用会引起panic; 196 | 7. 最后将statep值重置为零,然后释放所有的waiter; 197 | 198 | ### Wait方法 199 | 200 | ```go 201 | func (wg *WaitGroup) Wait() { 202 | statep, semap := wg.state() 203 | ... 204 | for { 205 | state := atomic.LoadUint64(statep) 206 | // 获取counter 207 | v := int32(state >> 32) 208 | // 获取waiter 209 | w := uint32(state) 210 | // counter为零,不需要等待直接返回 211 | if v == 0 { 212 | ... 213 | return 214 | } 215 | // 使用CAS将waiter加1 216 | if atomic.CompareAndSwapUint64(statep, state, state+1) { 217 | ... 218 | // 挂起等待唤醒 219 | runtime_Semacquire(semap) 220 | // 唤醒之后statep不为零,表示WaitGroup又被重复使用,这回panic 221 | if *statep != 0 { 222 | panic("sync: WaitGroup is reused before previous Wait has returned") 223 | } 224 | ... 225 | // 直接返回 226 | return 227 | } 228 | } 229 | } 230 | ``` 231 | 232 | 1. Wait方法首先也是调用state方法获取状态值; 233 | 2. 进入for循环之后Load statep的值,然后分别获取counter和counter; 234 | 3. 如果counter已经为零了,那么直接返回不需要等待; 235 | 4. counter不为零,那么使用CAS将waiter加1,由于CAS可能失败,所以for循环会再次的回到这里进行CAS,直到成功; 236 | 5. 调用runtime_Semacquire挂起等待唤醒; 237 | 6. `*statep != 0`唤醒之后statep不为零,表示WaitGroup又被重复使用,这会panic。需要注意的是waitgroup并不是不让重用,而是不能在wait方法还没运行完就开始重用。 238 | 239 | ### waitgroup使用小结 240 | 241 | 看完了waitgroup的add方法与wait方法,我们发现里面有很多校验,使用不当会导致panic,所以我们需要总结一下如何正确使用: 242 | 243 | * 不能将计数器设置为负数,否则会发生panic;注意有两种方式会导致计数器为负数,一是调用 Add 的时候传递一个负数,第二是调用 Done 方法的次数过多,超过了 WaitGroup 的计数值; 244 | * 在使用 WaitGroup 的时候,一定要等所有的 Add 方法调用之后再调用 Wait,否则就可能导致 panic; 245 | * wait还没结束就重用 WaitGroup。WaitGroup是可以重用的,但是需要等上一批的goroutine 都调用wait完毕后才能继续重用WaitGroup; 246 | 247 | ## 总结 248 | 249 | waitgroup里面的代码实际上是非常的简单的,这篇文章主要是由waitgroup引入了内存对齐这个概念。由waitgroup带我们看了在实际的代码中是如何利用内存对齐这个概念的,以及如何在32为操作系统中原子性的操作64位长的字段。 250 | 251 | 除了内存对齐的概念以外通过源码我们也了解到了使用waitgroup的时候需要怎么做才是符合规范的,不会引发panic。 252 | 253 | ## Reference 254 | 255 | http://blog.newbmiao.com/2020/02/10/dig101-golang-struct-memory-align.html 256 | 257 | https://gfw.go101.org/article/memory-layout.html 258 | 259 | https://golang.org/pkg/sync/atomic/#pkg-note-BUG 260 | 261 | https://en.wikipedia.org/wiki/Data_structure_alignment 262 | 263 | https://www.zhihu.com/question/27862634 264 | -------------------------------------------------------------------------------- /golang/7.Go语言时间轮的实现.md: -------------------------------------------------------------------------------- 1 | # Go语言中时间轮的实现 2 | 3 | 最近在工作中有一个需求,简单来说就是在短时间内会创建上百万个定时任务,创建的时候会将对应的金额相加,防止超售,需要过半个小时再去核对数据,如果数据对不上就需要将加上的金额再减回去。 4 | 5 | 这个需求如果用Go内置的Timer来做的话性能比较低下,因为Timer是使用最小堆来实现的,创建和删除的时间复杂度都为 O(log n)。如果使用时间轮的话则是O(1)性能会好很多。 6 | 7 | 对于时间轮来说,我以前写过一篇java版的时间轮算法分析:https://www.luozhiyun.com/archives/59,这次来看看Go语言的时间轮实现,顺便大家有兴趣的也可以对比一下两者的区别,以及我写文章的水平和一年多前有没有提升,哈哈哈。 8 | 9 | 时间轮的运用其实是非常的广泛的,在 Netty、Akka、Quartz、ZooKeeper、Kafka 等组件中都存在时间轮的踪影。下面用Go实现的时间轮是以Kafka的代码为原型来实现的,完整代码:https://github.com/devYun/timingwheel。 10 | 11 | ## 介绍 12 | 13 | ### 简单时间轮 14 | 15 | 在时间轮中存储任务的是一个环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表。定时任务列表是一个环形的双向链表,链表中的每一项表示的都是定时任务项,其中封装了真正的定时任务。 16 | 17 | 时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格个数是固定的,可用 wheelSize 来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式 tickMs×wheelSize 计算得出。 18 | 19 | 时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime 是 tickMs 的整数倍。currentTime指向的地方是表示到期的时间格,表示需要处理的时间格所对应的链表中的所有任务。 20 | 21 | 如下图是一个tickMs为1s,wheelSize等于10的时间轮,每一格里面放的是一个定时任务链表,链表里面存有真正的任务项: 22 | 23 | ![taskList](Go语言时间轮的实现/taskList.png) 24 | 25 | 初始情况下表盘指针 currentTime 指向时间格0,若时间轮的 tickMs 为 1ms 且 wheelSize 等于10,那么interval则等于10s。如下图此时有一个定时为2s的任务插进来会存放到时间格为2的任务链表中,用红色标记。随着时间的不断推移,指针 currentTime 不断向前推进,如果过了2s,那么 currentTime 会指向时间格2的位置,会将此时间格的任务链表获取出来处理。 26 | 27 | ![timewheel](Go语言时间轮的实现/timewheel.png) 28 | 29 | 如果当前的指针 currentTime 指向的是2,此时如果插入一个9s的任务进来,那么新来的任务会服用原来的时间格链表,会存放到时间格1中 30 | 31 | ![timewheelAdd9S](Go语言时间轮的实现/timewheelAdd9S.png) 32 | 33 | 这里所讲的时间轮都是简单时间轮,只有一层,总体时间范围在 currentTime 和 currentTime+interval 之间。如果现在有一个15s的定时任务是需要重新开启一个时间轮,设置一个时间跨度至少为15s的时间轮才够用。但是这样扩充是没有底线的,如果需要一个1万秒的时间轮,那么就需要一个这么大的数组去存放,不仅占用很大的内存空间,而且也会因为需要遍历这么大的数组从而拉低效率。 34 | 35 | 因此引入了层级时间轮的概念。 36 | 37 | ### 层级时间轮 38 | 39 | 如图是一个两层的时间轮,第二层时间轮也是由10个时间格组成,每个时间格的跨度是10s。第二层的时间轮的 tickMs 为第一层时间轮的 interval,即10s。每一层时间轮的 wheelSize 是固定的,都是10,那么第二层的时间轮的总体时间跨度 interval 为100s。 40 | 41 | 图中展示了每个时间格对应的过期时间范围, 我们可以清晰地看到, 第二层时间轮的第0个时间格的过期时间范围是 [0,9]。也就是说, 第二层时间轮的一个时间格就可以表示第一层时间轮的所有(10个)时间格; 42 | 43 | 如果向该时间轮中添加一个15s的任务,那么当第一层时间轮容纳不下时,进入第二层时间轮,并插入到过期时间为[10,19]的时间格中。 44 | 45 | ![timewheellevel2](Go语言时间轮的实现/timewheellevel2.png) 46 | 47 | 随着时间的流逝,当原本15s的任务还剩下5s的时候,这里就有一个时间轮降级的操作,此时第一层时间轮的总体时间跨度已足够,此任务被添加到第一层时间轮到期时间为5的时间格中,之后再经历5s后,此任务真正到期,最终执行相应的到期操作。 48 | 49 | ## 代码实现 50 | 51 | 因为我们这个Go语言版本的时间轮代码是仿照Kafka写的,所以在具体实现时间轮 TimingWheel 时还有一些小细节: 52 | 53 | * 时间轮的时间格中每个链表会有一个root节点用于简化边界条件。它是一个附加的链表节点,该节点作为第一个节点,它的值域中并不存储任何东西,只是为了操作的方便而引入的; 54 | * 除了第一层时间轮,其余高层时间轮的起始时间(startMs)都设置为创建此层时间轮时前面第一轮的 currentTime。每一层的 currentTime 都必须是 tickMs 的整数倍,如果不满足则会将 currentTime 修剪为 tickMs 的整数倍。修剪方法为:currentTime = startMs - (startMs % tickMs); 55 | * Kafka 中的定时器只需持有 TimingWheel 的第一层时间轮的引用,并不会直接持有其他高层的时间轮,但每一层时间轮都会有一个引用(overflowWheel)指向更高一层的应用; 56 | * Kafka 中的定时器使用了 DelayQueue 来协助推进时间轮。在操作中会将每个使用到的时间格中每个链表都加入 DelayQueue,DelayQueue 会根据时间轮对应的过期时间 expiration 来排序,最短 expiration 的任务会被排在 DelayQueue 的队头,通过单独线程来获取 DelayQueue 中到期的任务; 57 | 58 | ### 结构体 59 | 60 | ```go 61 | type TimingWheel struct { 62 | // 时间跨度,单位是毫秒 63 | tick int64 // in milliseconds 64 | // 时间轮个数 65 | wheelSize int64 66 | // 总跨度 67 | interval int64 // in milliseconds 68 | // 当前指针指向时间 69 | currentTime int64 // in milliseconds 70 | // 时间格列表 71 | buckets []*bucket 72 | // 延迟队列 73 | queue *delayqueue.DelayQueue 74 | // 上级的时间轮引用 75 | overflowWheel unsafe.Pointer // type: *TimingWheel 76 | 77 | exitC chan struct{} 78 | waitGroup waitGroupWrapper 79 | } 80 | ``` 81 | 82 | tick、wheelSize、interval、currentTime都比较好理解,buckets字段代表的是时间格列表,queue是一个延迟队列,所有的任务都是通过延迟队列来进行触发,overflowWheel是上层时间轮的引用。 83 | 84 | ```go 85 | type bucket struct { 86 | // 任务的过期时间 87 | expiration int64 88 | 89 | mu sync.Mutex 90 | // 相同过期时间的任务队列 91 | timers *list.List 92 | } 93 | ``` 94 | 95 | bucket里面实际上封装的是时间格里面的任务队列,里面放入的是相同过期时间的任务,到期后会将队列timers拿出来进行处理。这里有个有意思的地方是由于会有多个线程并发的访问bucket,所以需要用到原子类来获取int64位的值,为了保证32位系统上面读取64位数据的一致性,需要进行64位对齐。具体的可以看这篇:https://www.luozhiyun.com/archives/429,讲的是对内存对齐的思考。 96 | 97 | ```go 98 | type Timer struct { 99 | // 到期时间 100 | expiration int64 // in milliseconds 101 | // 要被执行的具体任务 102 | task func() 103 | // Timer所在bucket的指针 104 | b unsafe.Pointer // type: *bucket 105 | // bucket列表中对应的元素 106 | element *list.Element 107 | } 108 | ``` 109 | 110 | Timer是时间轮的最小执行单元,是定时任务的封装,到期后会调用task来执行任务。 111 | 112 | ![Group 37](Go语言时间轮的实现/Group 37.png) 113 | 114 | ### 初始化时间轮 115 | 116 | 例如现在初始化一个tick是1s,wheelSize是10的时间轮: 117 | 118 | ```go 119 | func main() { 120 | tw := timingwheel.NewTimingWheel(time.Second, 10) 121 | tw.Start() 122 | } 123 | 124 | func NewTimingWheel(tick time.Duration, wheelSize int64) *TimingWheel { 125 | // 将传入的tick转化成毫秒 126 | tickMs := int64(tick / time.Millisecond) 127 | // 如果小于零,那么panic 128 | if tickMs <= 0 { 129 | panic(errors.New("tick must be greater than or equal to 1ms")) 130 | } 131 | // 设置开始时间 132 | startMs := timeToMs(time.Now().UTC()) 133 | // 初始化TimingWheel 134 | return newTimingWheel( 135 | tickMs, 136 | wheelSize, 137 | startMs, 138 | delayqueue.New(int(wheelSize)), 139 | ) 140 | } 141 | 142 | func newTimingWheel(tickMs int64, wheelSize int64, startMs int64, queue *delayqueue.DelayQueue) *TimingWheel { 143 | // 初始化buckets的大小 144 | buckets := make([]*bucket, wheelSize) 145 | for i := range buckets { 146 | buckets[i] = newBucket() 147 | } 148 | // 实例化TimingWheel 149 | return &TimingWheel{ 150 | tick: tickMs, 151 | wheelSize: wheelSize, 152 | // currentTime必须是tickMs的倍数,所以这里使用truncate进行修剪 153 | currentTime: truncate(startMs, tickMs), 154 | interval: tickMs * wheelSize, 155 | buckets: buckets, 156 | queue: queue, 157 | exitC: make(chan struct{}), 158 | } 159 | } 160 | ``` 161 | 162 | 初始化十分简单,大家可以看看上面的代码注释即可。 163 | 164 | ### 启动时间轮 165 | 166 | 下面我们看看start方法: 167 | 168 | ```go 169 | func (tw *TimingWheel) Start() { 170 | // Poll会执行一个无限循环,将到期的元素放入到queue的C管道中 171 | tw.waitGroup.Wrap(func() { 172 | tw.queue.Poll(tw.exitC, func() int64 { 173 | return timeToMs(time.Now().UTC()) 174 | }) 175 | }) 176 | // 开启无限循环获取queue中C的数据 177 | tw.waitGroup.Wrap(func() { 178 | for { 179 | select { 180 | // 从队列里面出来的数据都是到期的bucket 181 | case elem := <-tw.queue.C: 182 | b := elem.(*bucket) 183 | // 时间轮会将当前时间 currentTime 往前移动到 bucket的到期时间 184 | tw.advanceClock(b.Expiration()) 185 | // 取出bucket队列的数据,并调用addOrRun方法执行 186 | b.Flush(tw.addOrRun) 187 | case <-tw.exitC: 188 | return 189 | } 190 | } 191 | }) 192 | } 193 | ``` 194 | 195 | 这里使用了util封装的一个Wrap方法,这个方法会起一个goroutines异步执行传入的函数,具体的可以到我上面给出的链接去看源码。 196 | 197 | Start方法会启动两个goroutines。第一个goroutines用来调用延迟队列的queue的Poll方法,这个方法会一直循环获取队列里面的数据,然后将到期的数据放入到queue的C管道中;第二个goroutines会无限循环获取queue中C的数据,如果C中有数据表示已经到期,那么会先调用advanceClock方法将当前时间 currentTime 往前移动到 bucket的到期时间,然后再调用Flush方法取出bucket中的队列,并调用addOrRun方法执行。 198 | 199 | ```go 200 | func (tw *TimingWheel) advanceClock(expiration int64) { 201 | currentTime := atomic.LoadInt64(&tw.currentTime) 202 | // 过期时间大于等于(当前时间+tick) 203 | if expiration >= currentTime+tw.tick { 204 | // 将currentTime设置为expiration,从而推进currentTime 205 | currentTime = truncate(expiration, tw.tick) 206 | atomic.StoreInt64(&tw.currentTime, currentTime) 207 | 208 | // Try to advance the clock of the overflow wheel if present 209 | // 如果有上层时间轮,那么递归调用上层时间轮的引用 210 | overflowWheel := atomic.LoadPointer(&tw.overflowWheel) 211 | if overflowWheel != nil { 212 | (*TimingWheel)(overflowWheel).advanceClock(currentTime) 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | advanceClock方法会根据到期时间来从新设置currentTime,从而推进时间轮前进。 219 | 220 | ```go 221 | func (b *bucket) Flush(reinsert func(*Timer)) { 222 | var ts []*Timer 223 | 224 | b.mu.Lock() 225 | // 循环获取bucket队列节点 226 | for e := b.timers.Front(); e != nil; { 227 | next := e.Next() 228 | 229 | t := e.Value.(*Timer) 230 | // 将头节点移除bucket队列 231 | b.remove(t) 232 | ts = append(ts, t) 233 | 234 | e = next 235 | } 236 | b.mu.Unlock() 237 | 238 | b.SetExpiration(-1) // TODO: Improve the coordination with b.Add() 239 | 240 | for _, t := range ts { 241 | reinsert(t) 242 | } 243 | } 244 | ``` 245 | 246 | Flush方法会根据bucket里面timers列表进行遍历插入到ts数组中,然后调用reinsert方法,这里是调用的addOrRun方法。 247 | 248 | ```go 249 | func (tw *TimingWheel) addOrRun(t *Timer) { 250 | // 如果已经过期,那么直接执行 251 | if !tw.add(t) { 252 | // 异步执行定时任务 253 | go t.task() 254 | } 255 | } 256 | ``` 257 | 258 | addOrRun会调用add方法检查传入的定时任务Timer是否已经到期,如果到期那么异步调用task方法直接执行。add方法我们下面会接着分析。 259 | 260 | 整个start执行流程如图: 261 | 262 | ![timewheel_start](Go语言时间轮的实现/timewheel_start.png) 263 | 264 | 1. start方法回启动一个goroutines调用poll来处理DelayQueue中到期的数据,并将数据放入到管道C中; 265 | 2. start方法启动第二个goroutines方法会循环获取DelayQueue中管道C的数据,管道C中实际上存放的是一个bucket,然后遍历bucket的timers列表,如果任务已经到期,那么异步执行,没有到期则重新放入到DelayQueue中。 266 | 267 | ### add task 268 | 269 | ```go 270 | func main() { 271 | tw := timingwheel.NewTimingWheel(time.Second, 10) 272 | tw.Start() 273 | // 添加任务 274 | tw.AfterFunc(time.Second*15, func() { 275 | fmt.Println("The timer fires") 276 | exitC <- time.Now().UTC() 277 | }) 278 | } 279 | ``` 280 | 281 | 我们通过AfterFunc方法添加一个15s的定时任务,如果到期了,那么执行传入的函数。 282 | 283 | ```go 284 | func (tw *TimingWheel) AfterFunc(d time.Duration, f func()) *Timer { 285 | t := &Timer{ 286 | expiration: timeToMs(time.Now().UTC().Add(d)), 287 | task: f, 288 | } 289 | tw.addOrRun(t) 290 | return t 291 | } 292 | ``` 293 | 294 | AfterFunc方法回根据传入的任务到期时间,以及到期需要执行的函数封装成Timer,调用addOrRun方法。addOrRun方法我们上面已经看过了,会根据到期时间来决定是否需要执行定时任务。 295 | 296 | 下面我们来看一下add方法: 297 | 298 | ```go 299 | func (tw *TimingWheel) add(t *Timer) bool { 300 | currentTime := atomic.LoadInt64(&tw.currentTime) 301 | // 已经过期 302 | if t.expiration < currentTime+tw.tick { 303 | // Already expired 304 | return false 305 | // 到期时间在第一层环内 306 | } else if t.expiration < currentTime+tw.interval { 307 | // Put it into its own bucket 308 | // 获取时间轮的位置 309 | virtualID := t.expiration / tw.tick 310 | b := tw.buckets[virtualID%tw.wheelSize] 311 | // 将任务放入到bucket队列中 312 | b.Add(t) 313 | // 如果是相同的时间,那么返回false,防止被多次插入到队列中 314 | if b.SetExpiration(virtualID * tw.tick) { 315 | // 将该bucket加入到延迟队列中 316 | tw.queue.Offer(b, b.Expiration()) 317 | } 318 | 319 | return true 320 | } else { 321 | // Out of the interval. Put it into the overflow wheel 322 | // 如果放入的到期时间超过第一层时间轮,那么放到上一层中去 323 | overflowWheel := atomic.LoadPointer(&tw.overflowWheel) 324 | if overflowWheel == nil { 325 | atomic.CompareAndSwapPointer( 326 | &tw.overflowWheel, 327 | nil, 328 | // 需要注意的是,这里tick变成了interval 329 | unsafe.Pointer(newTimingWheel( 330 | tw.interval, 331 | tw.wheelSize, 332 | currentTime, 333 | tw.queue, 334 | )), 335 | ) 336 | overflowWheel = atomic.LoadPointer(&tw.overflowWheel) 337 | } 338 | // 往上递归 339 | return (*TimingWheel)(overflowWheel).add(t) 340 | } 341 | } 342 | ``` 343 | 344 | add方法根据到期时间来分成了三部分,第一部分是小于当前时间+tick,表示已经到期,那么返回false执行任务即可; 345 | 346 | 第二部分的判断会根据expiration是否小于时间轮的跨度,如果小于的话表示该定时任务可以放入到当前时间轮中,通过取模找到buckets对应的时间格并放入到bucket队列中,SetExpiration方法会根据传入的参数来判断是否已经执行过延迟队列的Offer方法,防止重复插入; 347 | 348 | 第三部分表示该定时任务的时间跨度超过了当前时间轮,需要升级到上一层的时间轮中。需要注意的是,上一层的时间轮的tick是当前时间轮的interval,延迟队列还是同一个,然后设置为指针overflowWheel,并调用add方法往上层递归。 349 | 350 | 到这里时间轮已经讲完了,不过还有需要注意的地方,我们在用上面的时间轮实现中,使用了DelayQueue加环形队列的方式实现了时间轮。对定时任务项的插入和删除操作而言,TimingWheel时间复杂度为 O(1),在DelayQueue中的队列使用的是优先队列,时间复杂度是O(log n),但是由于buckets列表实际上是非常小的,所以并不会影响性能。 351 | 352 | ## Reference 353 | 354 | https://github.com/RussellLuo/timingwheel 355 | 356 | https://zhuanlan.zhihu.com/p/121483218 357 | 358 | https://github.com/apache/kafka/tree/3cdc78e6bb1f83973a14ce1550fe3874f7348b05/core/src/main/scala/kafka/utils/timer -------------------------------------------------------------------------------- /golang/Go中内存分配/Group 37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/Group 37.png -------------------------------------------------------------------------------- /golang/Go中内存分配/Large Object Allocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/Large Object Allocation.png -------------------------------------------------------------------------------- /golang/Go中内存分配/allocCache-1611497692285.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/allocCache-1611497692285.png -------------------------------------------------------------------------------- /golang/Go中内存分配/allocCache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/allocCache.png -------------------------------------------------------------------------------- /golang/Go中内存分配/mcache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/mcache.png -------------------------------------------------------------------------------- /golang/Go中内存分配/mcentral-1747885.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/mcentral-1747885.png -------------------------------------------------------------------------------- /golang/Go中内存分配/mcentral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/mcentral.png -------------------------------------------------------------------------------- /golang/Go中内存分配/mchache2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/mchache2.png -------------------------------------------------------------------------------- /golang/Go中内存分配/mheap-1747918.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/mheap-1747918.png -------------------------------------------------------------------------------- /golang/Go中内存分配/mheap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中内存分配/mheap.png -------------------------------------------------------------------------------- /golang/Go中定时器实现原理(未完)/new_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中定时器实现原理(未完)/new_timer.png -------------------------------------------------------------------------------- /golang/Go中定时器实现原理(未完)/timer1.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中定时器实现原理(未完)/timer1.13.png -------------------------------------------------------------------------------- /golang/Go中的网络轮询器/Accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中的网络轮询器/Accept.png -------------------------------------------------------------------------------- /golang/Go中的网络轮询器/listen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中的网络轮询器/listen.png -------------------------------------------------------------------------------- /golang/Go中的网络轮询器/multiplexing model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中的网络轮询器/multiplexing model.png -------------------------------------------------------------------------------- /golang/Go中的网络轮询器/netFD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中的网络轮询器/netFD.png -------------------------------------------------------------------------------- /golang/Go中的网络轮询器/pollCache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go中的网络轮询器/pollCache.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/Linux_stack-1617529674577.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/Linux_stack-1617529674577.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack-8316529.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack-8316529.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack-8318004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack-8318004.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack10-1618750698509.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack10-1618750698509.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack10-1618753456961.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack10-1618753456961.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack10.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack2.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack3.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack4.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack5.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack6-1618668525016.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack6-1618668525016.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack6.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack7.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack8-1618749781455.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack8-1618749781455.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack8.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack9-1618749857402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack9-1618749857402.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/call stack9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/call stack9.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/function.png -------------------------------------------------------------------------------- /golang/Go语言函数对用与接口/image-20210412204906209.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言函数对用与接口/image-20210412204906209.png -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器.md: -------------------------------------------------------------------------------- 1 | # Go语言实现布谷鸟过滤器 2 | 3 | > 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 4 | 5 | ## 介绍 6 | 7 | 在我们工作中,如果遇到如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断一般想到的是将集合中所有元素保存起来,然后通过比较确定。如果通过性能最好的Hash表来进行判断,那么随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。 8 | 9 | 所以很多时候会选择使用布隆过滤器来做这件事。布隆过滤器通过一个固定大小的二进制向量或者位图(bitmap),然后通过映射函数来将存储到 bitmap 中的键值进行映射大大减少了空间成本,布隆过滤器存储空间和插入/查询时间都是常数 O(K)。但是随着存入的元素数量增加,布隆过滤器误算率会随之增加,并且也不能删除元素。 10 | 11 | 想要体验布隆过滤器的插入步骤的可以看看这里:https://www.jasondavies.com/bloomfilter/ 12 | 13 | 于是布谷鸟过滤器(Cuckoo filter)华丽降世了。在论文《Cuckoo Filter: Practically Better Than Bloom》中直接说到布隆过滤器的缺点: 14 | 15 | > A limitation of standard Bloom filters is that one cannot remove existing items without rebuilding the entire filter. 16 | 17 | 论文中也提到了布谷鸟过滤器4大优点: 18 | 19 | > 1. It supports adding and removing items dynamically; 20 | > 2. 2. It provides higher lookup performance than traditional Bloom filters, even when close to full (e.g., 95% space utilized); 21 | > 3. It is easier to implement than alternatives such as the quotient filter; and 22 | > 4. It uses less space than Bloom filters in many practical applications, if the target false positive rate is less than 3%. 23 | 24 | 翻译如下: 25 | 26 | 1. 支持动态的新增和删除元素; 27 | 2. 提供了比传统布隆过滤器更高的查找性能,即使在接近满的情况下(比如空间利用率达到 95% 的时候); 28 | 3. 更容易实现; 29 | 4. 如果要求错误率小于3%,那么在许多实际应用中,它比布隆过滤器占用的空间更小。 30 | 31 | ## 实现原理 32 | 33 | ### 简单工作原理 34 | 35 | 可以简单的把布谷鸟过滤器里面有两个 hash 表T1、T2,两个 hash 表对应两个 hash 函数H1、H2。 36 | 37 | 具体的插入步骤如下: 38 | 39 | 1. 当一个不存在的元素插入的时候,会先根据 H1 计算出其在 T1 表的位置,如果该位置为空则可以放进去。 40 | 2. 如果该位置不为空,则根据 H2 计算出其在 T2 表的位置,如果该位置为空则可以放进去。 41 | 3. 如果T1 表和 T2 表的位置元素都不为空,那么就随机的选择一个 hash 表将其元素踢出。 42 | 4. 被踢出的元素会循环的去找自己的另一个位置,如果被暂了也会随机选择一个将其踢出,被踢出的元素又会循环找位置; 43 | 5. 如果出现循环踢出导致放不进元素的情况,那么会设置一个阈值,超出了某个阈值,就认为这个 hash 表已经几乎满了,这时候就需要对它进行扩容,重新放置所有元素。 44 | 45 | 下面举一个例子来说明: 46 | 47 | ![Cuckoo Filter Insert](Go语言实现布谷鸟过滤器/Cuckoo Filter Insert.png) 48 | 49 | 如果想要插入一个元素Z到过滤器里: 50 | 51 | 1. 首先会将Z进行 hash 计算,发现 T1 和 T2 对应的槽位1和槽位2都已经被占了; 52 | 2. 随机将 T1 中的槽位1中的元素 X 踢出,X 的 T2 对应的槽位4已经被元素 3 占了; 53 | 3. 将 T2 中的槽位4中的元素 3 踢出,元素 3 在 hash 计算之后发现 T1 的槽位6是空的,那么将元素3放入到 T1 的槽位6中。 54 | 55 | 当 Z 插入完毕之后如下: 56 | 57 | ![Cuckoo Filter Insert2](Go语言实现布谷鸟过滤器/Cuckoo Filter Insert2.png) 58 | 59 | ### 布谷鸟过滤器 60 | 61 | 布谷鸟过滤器和上面的实现原理结构是差不多的,不同的是上面的数组结构会存储整个元素,而布谷鸟过滤器中只会存储元素的几个 bit ,称作指纹信息。这里是牺牲了数据的精确性换取了空间效率。 62 | 63 | 上面的实现方案中,hash 表中每个槽位只能存放一个元素,空间利用率只有50%,而在布谷鸟过滤器中每个槽位可以存放多个元素,从一维变成了二维。论文中表示: 64 | 65 | > With k = 2 hash functions, the load factor α is 50% when the bucket size b = 1 (i.e., the hash table is directly mapped), but increases to 84%, 95% or 98% respectively using bucket size b = 2, 4 or 8. 66 | 67 | 也就是当有两个 hash 函数的时候,使用一维数组空间利用率只有50%,当每个槽位可以存放2,4,8个元素的时候,空间利用率就会飙升到 84%,95%,98%。 68 | 69 | 如下图,表示的是一个二维数组,每个槽位可以存放 4 个元素,和上面的实现有所不同的是,没有采用两个数组来存放,而是只用了一个: 70 | 71 | ![CuckooFilter](Go语言实现布谷鸟过滤器/CuckooFilter.png) 72 | 73 | 说完了数据结构的改变,下面再说说位置计算的改变。 74 | 75 | 我们在上面简单实现的位置计算公式是这样做的: 76 | 77 | ``` 78 | p1 = hash1(x) % 数组长度 79 | p2 = hash2(x) % 数组长度 80 | ``` 81 | 82 | 而布谷鸟过滤器计算位置公式可以在论文中看到是这样: 83 | 84 | > f = fingerprint(x); 85 | > 86 | > i1 = hash(x); 87 | > 88 | > i2 = i1 ⊕ hash( f); 89 | 90 | 我们可以看到在计算位置 i2 的时候是通过 i1 和元素 X 对应的指纹信息取异或计算出来。指纹信息在上面已经解释过了,是元素 X 的几个 bit ,牺牲了一定精度,但是换取了空间。 91 | 92 | 那么这里为什么需要用到异或呢?因为这样可以用到异或的自反性:`A ⊕ B ⊕ B = A`,这样就不需要知道当前的位置是 i1 还是 i2,只需要将当前的位置和 hash(f) 进行异或计算就可以得到另一个位置。 93 | 94 | 这里有个细节需要注意的是,计算 i2 的时候是需要先将元素 X 的 fingerprint 进行 hash ,然后才取异或,论文也说明了: 95 | 96 | > If the alternate location were calculated by “i⊕fingerprint” without hashing the fingerprint, the items kicked out from nearby buckets would land close to each other in the table, if the size of the fingerprint is small compared to the table size. 97 | 98 | 如果直接进行异或处理,那么很可能 i1 和 i2 的位置相隔很近,尤其是在比较小的 hash 表中,这样无形之中增加了碰撞的概率。 99 | 100 | 除此之外还有一个约束条件是布谷鸟过滤器强制数组的长度必须是 2 的指数,所以在布谷鸟过滤器中不需要对数组的长度取模,取而代之的是取 hash 值的最后 n 位。 101 | 102 | 如一个布谷鸟过滤器中数组的长度2^8即256,那么取 hash 值的最后 n 位即:`hash & 255`这样就可以得到最终的位置信息。如下最后得到位置信息是 23 : 103 | 104 | ![position](Go语言实现布谷鸟过滤器/position.png) 105 | 106 | ## 代码实现 107 | 108 | ### 数据结构 109 | 110 | ```go 111 | const bucketSize = 4 112 | type fingerprint byte 113 | // 二维数组,大小是4 114 | type bucket [bucketSize]fingerprint 115 | 116 | type Filter struct { 117 | // 一维数组 118 | buckets []bucket 119 | // Filter 中已插入的元素 120 | count uint 121 | // 数组buckets长度中对应二进制包含0的个数 122 | bucketPow uint 123 | } 124 | ``` 125 | 126 | 在这里我们假定一个指纹 fingerprint 占用的字节数是 1byte ,每个位置有 4 个座位。 127 | 128 | ### 初始化 129 | 130 | ```go 131 | var ( 132 | altHash = [256]uint{} 133 | masks = [65]uint{} 134 | ) 135 | 136 | func init() { 137 | for i := 0; i < 256; i++ { 138 | // 用于缓存 256 个fingerprint的hash信息 139 | altHash[i] = (uint(metro.Hash64([]byte{byte(i)}, 1337))) 140 | } 141 | for i := uint(0); i <= 64; i++ { 142 | // 取 hash 值的最后 n 位 143 | masks[i] = (1 << i) - 1 144 | } 145 | } 146 | ``` 147 | 148 | 这个 init 函数会缓存初始化两个全局变量 altHash 和 masks。因为 fingerprint 长度是 1byte ,所以在初始化 altHash 的时候使用一个 256 大小的数组取缓存对应的 hash 信息,避免每次都需要重新计算;masks 是用来取 hash 值的最后 n 位,稍后会用到。 149 | 150 | 我们会使用一个 NewFilter 函数,通过传入过滤器可容纳大小来获取过滤器 Filter: 151 | 152 | ```go 153 | func NewFilter(capacity uint) *Filter { 154 | // 计算 buckets 数组大小 155 | capacity = getNextPow2(uint64(capacity)) / bucketSize 156 | if capacity == 0 { 157 | capacity = 1 158 | } 159 | buckets := make([]bucket, capacity) 160 | return &Filter{ 161 | buckets: buckets, 162 | count: 0, 163 | // 获取 buckets 数组大小的二进制中以 0 结尾的个数 164 | bucketPow: uint(bits.TrailingZeros(capacity)), 165 | } 166 | } 167 | ``` 168 | 169 | NewFilter 函数会通过 getNextPow2 将 capacity 调整到 2 的指数倍,如果传入的 capacity 是 9 ,那么调用 getNextPow2 后会返回 16;然后计算好 buckets 数组长度,实例化 Filter 返回;bucketPow 返回的是二进制中以 0 结尾的个数,因为 capacity 是 2 的指数倍,所以 bucketPow 是 capacity 二进制的位数减 1。 170 | 171 | ### 插入元素 172 | 173 | ```go 174 | func (cf *Filter) Insert(data []byte) bool { 175 | // 获取 data 的 fingerprint 以及 位置 i1 176 | i1, fp := getIndexAndFingerprint(data, cf.bucketPow) 177 | // 将 fingerprint 插入到 Filter 的 buckets 数组中 178 | if cf.insert(fp, i1) { 179 | return true 180 | } 181 | // 获取位置 i2 182 | i2 := getAltIndex(fp, i1, cf.bucketPow) 183 | // 将 fingerprint 插入到 Filter 的 buckets 数组中 184 | if cf.insert(fp, i2) { 185 | return true 186 | } 187 | // 插入失败,那么进行循环插入踢出元素 188 | return cf.reinsert(fp, randi(i1, i2)) 189 | } 190 | 191 | func (cf *Filter) insert(fp fingerprint, i uint) bool { 192 | // 获取 buckets 中的槽位进行插入 193 | if cf.buckets[i].insert(fp) { 194 | // Filter 中元素个数+1 195 | cf.count++ 196 | return true 197 | } 198 | return false 199 | } 200 | 201 | func (b *bucket) insert(fp fingerprint) bool { 202 | // 遍历槽位的 4 个元素,如果为空则插入 203 | for i, tfp := range b { 204 | if tfp == nullFp { 205 | b[i] = fp 206 | return true 207 | } 208 | } 209 | return false 210 | } 211 | ``` 212 | 213 | 1. getIndexAndFingerprint 函数会获取 data 的指纹 fingerprint,以及位置 i1; 214 | 215 | 2. 然后调用 insert 插入到 Filter 的 buckets 数组中,如果 buckets 数组中对应的槽位 i1 的 4 个元素已经满了,那么尝试获取位置 i2 ,并将元素尝试插入到 buckets 数组中对应的槽位 i2 中; 216 | 3. 对应的槽位 i2 也满了,那么 调用 reinsert 方法随机获取槽位 i1、i2 中的某个位置进行抢占,然后将老元素踢出并循环重复插入。 217 | 218 | 下面看看 getIndexAndFingerprint 是如何获取 fingerprint 以及槽位 i1: 219 | 220 | ```go 221 | func getIndexAndFingerprint(data []byte, bucketPow uint) (uint, fingerprint) { 222 | // 将 data 进行hash 223 | hash := metro.Hash64(data, 1337) 224 | // 取 hash 的指纹信息 225 | fp := getFingerprint(hash) 226 | // 取 hash 高32位,对 hash 的高32位进行取与获取槽位 i1 227 | i1 := uint(hash>>32) & masks[bucketPow] 228 | return i1, fingerprint(fp) 229 | } 230 | // 取 hash 的指纹信息 231 | func getFingerprint(hash uint64) byte { 232 | fp := byte(hash%255 + 1) 233 | return fp 234 | } 235 | ``` 236 | 237 | getIndexAndFingerprint 中对 data 进行 hash 完后会对其结果取模获取指纹信息,然后再取 hash 值的高 32 位进行取与,获取槽位 i1。masks 在初始化的时候已经看过了,`masks[bucketPow]` 获取的二进制结果全是 1 ,用来取 hash 的低位的值。 238 | 239 | 假如初始化传入的 capacity 是1024,那么计算到 bucketPow 是 8,对应取到 `masks[8] = (1 << 8) - 1` 结果是 255 ,二进制是`1111,1111`,和 hash 的高 32 取与 得到最后 buckets 中的槽位 i1 : 240 | 241 | ![position](Go语言实现布谷鸟过滤器/position.png) 242 | 243 | 244 | 245 | ```go 246 | func getAltIndex(fp fingerprint, i uint, bucketPow uint) uint { 247 | mask := masks[bucketPow] 248 | hash := altHash[fp] & mask 249 | return i ^ hash 250 | } 251 | ``` 252 | 253 | getAltIndex 中获取槽位是通过使用 altHash 来获取指纹信息的 hash 值,然后取异或后返回槽位值。需要注意的是,这里由于异或的特性,所以传入的不管是槽位 i1,还是槽位 i2 都可以返回对应的另一个槽位。 254 | 255 | 下面看看循环踢出插入 reinsert: 256 | 257 | ```go 258 | const maxCuckooCount = 500 259 | 260 | func (cf *Filter) reinsert(fp fingerprint, i uint) bool { 261 | // 默认循环 500 次 262 | for k := 0; k < maxCuckooCount; k++ { 263 | // 随机从槽位中选取一个元素 264 | j := rand.Intn(bucketSize) 265 | oldfp := fp 266 | // 获取槽位中的值 267 | fp = cf.buckets[i][j] 268 | // 将当前循环的值插入 269 | cf.buckets[i][j] = oldfp 270 | 271 | // 获取另一个槽位 272 | i = getAltIndex(fp, i, cf.bucketPow) 273 | if cf.insert(fp, i) { 274 | return true 275 | } 276 | } 277 | return false 278 | } 279 | ``` 280 | 281 | 这里会最大循环 500 次获取槽位信息。因为每个槽位最多可以存放 4 个元素,所以使用 rand 随机从 4 个位置中取一个元素踢出,然后将当次循环的元素插入,再获取被踢出元素的另一个槽位信息,再调用 insert 进行插入。 282 | 283 | ![Cuckoo Filter Insert3](Go语言实现布谷鸟过滤器/Cuckoo Filter Insert3.png) 284 | 285 | 上图展示了元素 X 在插入到 hash 表的时候,hash 两次发现对应的槽位 0 和 3 都已经满了,那么随机抢占了槽位 3 其中一个元素,被抢占的元素重新 hash 之后插入到槽位 5 的第三个位置上。 286 | 287 | ### 查询数据 288 | 289 | 查询数据的时候,就是看看对应的位置上有没有对应的指纹信息: 290 | 291 | ```go 292 | func (cf *Filter) Lookup(data []byte) bool { 293 | // 获取槽位 i1 以及指纹信息 294 | i1, fp := getIndexAndFingerprint(data, cf.bucketPow) 295 | // 遍历槽位中 4 个位置,查看有没有相同元素 296 | if cf.buckets[i1].getFingerprintIndex(fp) > -1 { 297 | return true 298 | } 299 | // 获取另一个槽位 i2 300 | i2 := getAltIndex(fp, i1, cf.bucketPow) 301 | // 遍历槽位 i2 中 4 个位置,查看有没有相同元素 302 | return cf.buckets[i2].getFingerprintIndex(fp) > -1 303 | } 304 | 305 | func (b *bucket) getFingerprintIndex(fp fingerprint) int { 306 | for i, tfp := range b { 307 | if tfp == fp { 308 | return i 309 | } 310 | } 311 | return -1 312 | } 313 | ``` 314 | 315 | ### 删除数据 316 | 317 | 删除数据的时候,也只是抹掉该槽位上的指纹信息: 318 | 319 | ```go 320 | func (cf *Filter) Delete(data []byte) bool { 321 | // 获取槽位 i1 以及指纹信息 322 | i1, fp := getIndexAndFingerprint(data, cf.bucketPow) 323 | // 尝试删除指纹信息 324 | if cf.delete(fp, i1) { 325 | return true 326 | } 327 | // 获取槽位 i2 328 | i2 := getAltIndex(fp, i1, cf.bucketPow) 329 | // 尝试删除指纹信息 330 | return cf.delete(fp, i2) 331 | } 332 | 333 | func (cf *Filter) delete(fp fingerprint, i uint) bool { 334 | // 遍历槽位 4个元素,尝试删除指纹信息 335 | if cf.buckets[i].delete(fp) { 336 | if cf.count > 0 { 337 | cf.count-- 338 | } 339 | return true 340 | } 341 | return false 342 | } 343 | 344 | func (b *bucket) delete(fp fingerprint) bool { 345 | for i, tfp := range b { 346 | // 指纹信息相同,将此槽位置空 347 | if tfp == fp { 348 | b[i] = nullFp 349 | return true 350 | } 351 | } 352 | return false 353 | } 354 | ``` 355 | 356 | ### 缺点 357 | 358 | 实现完布谷鸟过滤器后,我们不妨想一下,如果布谷鸟过滤器对同一个元素进行多次连续的插入会怎样? 359 | 360 | 那么这个元素会霸占两个槽位上的所有位置,最后在插入第 9 个相同元素的时候,会一直循环挤兑,直到最大循环次数,然后返回一个 false: 361 | 362 | ![Cuckoo Filter Insert4](Go语言实现布谷鸟过滤器/Cuckoo Filter Insert4.png) 363 | 364 | 如果插入之前做一次检查能不能解决问题呢?这样确实不会出现循环挤兑的情况,但是会出现一定概率的误判情况。 365 | 366 | 由上面的实现我们可以知道,在每个位置里设置的指纹信息是 1byte,256 种可能,如果两个元素的 hash 位置相同,指纹相同,那么这个插入检查会认为它们是相等的导致认为元素已存在。 367 | 368 | 事实上,我们可以通过调整指纹信息的保存量来降低误判情况,如在上面的实现中,指纹信息是 1byte 保存8位信息误判概率是0.03,当指纹信息增加到 2bytes 保存16位信息误判概率会降低至 0.0001。 369 | 370 | ## Reference 371 | 372 | Cuckoo Filter: Practically Better Than Bloom https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf 373 | 374 | Cuckoo Hashing Visualization http://www.lkozma.net/cuckoo_hashing_visualization/ 375 | 376 | Cuckoo Filter https://github.com/seiflotfy/cuckoofilter 377 | 378 | -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert.png -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert2.png -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert3.png -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言实现布谷鸟过滤器/Cuckoo Filter Insert4.png -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器/CuckooFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言实现布谷鸟过滤器/CuckooFilter.png -------------------------------------------------------------------------------- /golang/Go语言实现布谷鸟过滤器/position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言实现布谷鸟过滤器/position.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327132559064.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327132559064.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327132807943.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327132807943.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327144615922.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327144615922.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327145625294.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327145625294.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327145652092.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327145652092.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327152443777.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327152443777.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327152534498.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327152534498.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/image-20210327152857867.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/image-20210327152857867.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/preempt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/preempt.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/stw_preempt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/stw_preempt.png -------------------------------------------------------------------------------- /golang/Go语言抢占调度/sysmon_preempt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言抢占调度/sysmon_preempt.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/Group 37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/Group 37.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/bucket.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/taskList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/taskList.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/timewheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/timewheel.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/timewheelAdd9S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/timewheelAdd9S.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/timewheel_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/timewheel_start.png -------------------------------------------------------------------------------- /golang/Go语言时间轮的实现/timewheellevel2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言时间轮的实现/timewheellevel2.png -------------------------------------------------------------------------------- /golang/Go语言调度器/GMP-3738208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/GMP-3738208.png -------------------------------------------------------------------------------- /golang/Go语言调度器/M_bind_CPU-1613744972579.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/M_bind_CPU-1613744972579.png -------------------------------------------------------------------------------- /golang/Go语言调度器/M_bind_CPU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/M_bind_CPU.png -------------------------------------------------------------------------------- /golang/Go语言调度器/execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/execute.png -------------------------------------------------------------------------------- /golang/Go语言调度器/mstart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/mstart.png -------------------------------------------------------------------------------- /golang/Go语言调度器/runq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/runq.png -------------------------------------------------------------------------------- /golang/Go语言调度器/schedule-1613903287779.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/schedule-1613903287779.png -------------------------------------------------------------------------------- /golang/Go语言调度器/schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/Go语言调度器/schedule.png -------------------------------------------------------------------------------- /golang/图解Go的互斥锁/Group 1-1607867443891.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/图解Go的互斥锁/Group 1-1607867443891.png -------------------------------------------------------------------------------- /golang/图解Go的互斥锁/Group 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/图解Go的互斥锁/Group 5.png -------------------------------------------------------------------------------- /golang/图解Go的互斥锁/Group 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/图解Go的互斥锁/Group 6.png -------------------------------------------------------------------------------- /golang/图解Go的互斥锁/Group 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/golang/图解Go的互斥锁/Group 7.png -------------------------------------------------------------------------------- /golang/如何阅读Go源码.md: -------------------------------------------------------------------------------- 1 | # 如何编译调试Go runtime源码 2 | 3 | 有朋友问我阅读源码,该怎么调试?这次我们简单看看如何编译调试 Go 的 runtime 源码,感兴趣的朋友可以自己手动操作一下。 4 | 5 | ## 编译修改 Go 源码进行调试 6 | 7 | ### 初次下载编译 8 | 9 | 我使用的是 centos 环境,所以需要先安装一下 `yum -y install gcc`; 10 | 11 | 然后下载 go 源码: 12 | 13 | ```sh 14 | [root@localhost src]# git clone https://github.com/golang/go.git 15 | 16 | #进入到src 目录执行 17 | [root@localhost src]# ./all.bash 18 | 19 | [root@localhost src]# ./all.bash 20 | Building Go cmd/dist using /usr/local/go. (go1.15.8 linux/amd64) 21 | Building Go toolchain1 using /usr/local/go. 22 | Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. 23 | Building Go toolchain2 using go_bootstrap and Go toolchain1. 24 | Building Go toolchain3 using go_bootstrap and Go toolchain2. 25 | Building packages and commands for linux/amd64. 26 | ... 27 | 28 | ##### API check 29 | Go version is "go1.15.10", ignoring -next /data/learn/go/api/next.txt 30 | 31 | ALL TESTS PASSED 32 | --- 33 | Installed Go for linux/amd64 in /data/learn/go 34 | Installed commands in /data/learn/go/bin 35 | *** You need to add /data/learn/go/bin to your PATH. 36 | ``` 37 | 38 | 编译好的 go 和 gofmt 在 bin 目录下: 39 | 40 | ```sh 41 | [root@localhost src]# cd ../bin/ 42 | [root@localhost bin]# ls 43 | go gofmt 44 | ``` 45 | 46 | 为了防止我们修改的 go 和过去安装的 go 冲突,创建 mygo 软连接,指向修改的 go : 47 | 48 | ```sh 49 | [root@localhost bin]# mkdir -p ~/mygo/bin 50 | 51 | [root@localhost bin]# cd ~/testgo/bin 52 | 53 | [root@localhost bin]# ln -sf /data/learn/go/bin/go mygo 54 | ``` 55 | 56 | 最后,把`~/testgo/bin`加入到`PATH`: 57 | 58 | ```sh 59 | [root@localhost bin]# vim /etc/profile 60 | 61 | export PATH=$PATH:/data/learn/mygo/bin 62 | 63 | source /etc/profile 64 | ``` 65 | 66 | 运行下mygo,查看一下版本: 67 | 68 | ```sh 69 | [root@localhost bin]# mygo version 70 | go version go1.15.10 linux/amd64 71 | ``` 72 | 73 | ### GODEBUG 74 | 75 | 我们在修改源码的时候,可以借助 GODEBUG 变量来打印调试信息。 76 | 77 | #### schedtrace 78 | 79 | > schedtrace: setting schedtrace=X causes the scheduler to emit a single line to standard 80 | > error every X milliseconds, summarizing the scheduler state. 81 | 82 | schedtrace=X 表示运行时每 X 毫秒打印一行调度器的摘要信息到标准 err 输出中。 83 | 84 | 如设置 schedtrace=1000 程序开始后每个一秒就会打印一行调度器的概要信息 : 85 | 86 | ```sh 87 | [root@localhost gotest]# GOMAXPROCS=1 GODEBUG=schedtrace=1000 mygo run main.go SCHED 0ms: gomaxprocs=1 idleprocs=0 threads=4 spinningthreads=0 idlethreads=0 runqueue=0 [2] 88 | # command-line-arguments 89 | SCHED 0ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=1 runqueue=0 [2] 90 | SCHED 0ms: gomaxprocs=1 idleprocs=0 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 [2] 91 | ``` 92 | 93 | 0ms :自从程序开始的毫秒数; 94 | 95 | gomaxprocs=1:配置的处理器数; 96 | 97 | idleprocs=0:空闲的 P(处理器)数; 98 | 99 | threads=3: 运行期管理的线程数,目前6个线程; 100 | 101 | spinningthreads=0:执行抢占的线程数; 102 | 103 | idlethreads=1:空闲的线程数; 104 | 105 | runqueue=0: 在全局的run队列中的 goroutine 数; 106 | 107 | [2]: 本地run队列中的goroutine数,表示有两个在等待; 108 | 109 | #### scheddetail 110 | 111 | > scheddetail: setting schedtrace=X and scheddetail=1 causes the scheduler to emit 112 | > detailed multiline info every X milliseconds, describing state of the scheduler, 113 | > processors, threads and goroutines. 114 | 115 | schedtrace 和 scheddetail 一起设置可以提供处理器P,线程M和goroutine G的细节。 116 | 117 | 例如: 118 | 119 | ```sh 120 | [root@localhost gotest]# GOMAXPROCS=1 GODEBUG=schedtrace=1000,scheddetail=1 mygo run main.go 121 | SCHED 0ms: gomaxprocs=1 idleprocs=0 threads=4 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 122 | P0: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=2 gfreecnt=0 timerslen=0 123 | M3: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1 124 | M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1 125 | M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=17 126 | M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=1 127 | G1: status=1(chan receive) m=-1 lockedm=0 128 | G17: status=6() m=1 lockedm=1 129 | G2: status=1() m=-1 lockedm=-1 130 | G3: status=1() m=-1 lockedm=-1 131 | G4: status=4(GC scavenge wait) m=-1 lockedm=-1 132 | ... 133 | ``` 134 | 135 | 下面我们先看看 G 代表的意思: 136 | 137 | * status:G 的运行状态; 138 | 139 | * m:隶属哪一个 M; 140 | 141 | * lockedm:是否有锁定 M; 142 | 143 | G 的运行状态大概有这些含义: 144 | 145 | ```go 146 | const ( 147 | // 刚刚被分配并且还没有被初始化 148 | _Gidle = iota // 0 149 | // 没有执行代码,没有栈的所有权,存储在运行队列中 150 | _Grunnable // 1 151 | // 可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P 152 | _Grunning // 2 153 | // 正在执行系统调用,拥有栈的所有权,没有执行用户代码, 154 | // 被赋予了内核线程 M 但是不在运行队列上 155 | _Gsyscall // 3 156 | // 由于运行时而被阻塞,没有执行用户代码并且不在运行队列上, 157 | // 但是可能存在于 Channel 的等待队列上 158 | _Gwaiting // 4 159 | // 表示当前goroutine没有被使用,没有执行代码,可能有分配的栈 160 | _Gdead // 6 161 | // 栈正在被拷贝,没有执行代码,不在运行队列上 162 | _Gcopystack // 8 163 | // 由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒 164 | _Gpreempted // 9 165 | // GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在 166 | _Gscan = 0x1000 167 | ... 168 | ) 169 | ``` 170 | 171 | 如果你看不懂的话,那么需要去看看我的调度循环的解析了:《详解Go语言调度循环源码实现 https://www.luozhiyun.com/archives/448》 172 | 173 | M 代表的意思: 174 | 175 | ``` 176 | M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=1 177 | ``` 178 | 179 | - p:隶属哪一个 P; 180 | - curg:当前正在使用哪个 G; 181 | - mallocing:是否正在分配内存; 182 | - throwing:是否抛出异常; 183 | - preemptoff:不等于空字符串("")的话,保持 curg 在这个 m 上运行; 184 | - runqsize:运行队列中的 G 数量; 185 | - spinning:是否在抢占 G; 186 | 187 | P 代表的意思: 188 | 189 | ``` 190 | P0: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=2 gfreecnt=0 timerslen=0 191 | ``` 192 | 193 | - status:P 的运行状态。 194 | - schedtick:P 的调度次数。 195 | - syscalltick:P 的系统调用次数。 196 | - m:隶属哪一个 M。 197 | - runqsize:运行队列中的 G 数量。 198 | - gfreecnt:可用的G(状态为 Gdead)。 199 | 200 | P 的 status 状态代表的含义: 201 | 202 | ```go 203 | const ( 204 | // 表示P没有运行用户代码或者调度器 205 | _Pidle = iota 206 | // 被线程 M 持有,并且正在执行用户代码或者调度器 207 | _Prunning 208 | // 没有执行用户代码,当前线程陷入系统调用 209 | _Psyscall 210 | // 被线程 M 持有,当前处理器由于垃圾回收 STW 被停止 211 | _Pgcstop 212 | // 当前处理器已经不被使用 213 | _Pdead 214 | ) 215 | ``` 216 | 217 | ### 修改编译 218 | 219 | 比方说我们在 channel 这里做一下修改加上一个 print 打印: 220 | 221 | ```go 222 | func makechan(t *chantype, size int) *hchan { 223 | ... 224 | if debug.schedtrace > 0 { 225 | print("bearluo makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n") 226 | } 227 | ... 228 | return c 229 | } 230 | ``` 231 | 232 | 然后进入到 go 的 src 目录下重新编译: 233 | 234 | ```sh 235 | [root@localhost src]# ./make.bash 236 | Building Go cmd/dist using /usr/local/go. (go1.15.8 linux/amd64) 237 | Building Go toolchain1 using /usr/local/go. 238 | Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. 239 | Building Go toolchain2 using go_bootstrap and Go toolchain1. 240 | Building Go toolchain3 using go_bootstrap and Go toolchain2. 241 | Building packages and commands for linux/amd64. 242 | --- 243 | Installed Go for linux/amd64 in /data/learn/go 244 | Installed commands in /data/learn/go/bin 245 | ``` 246 | 247 | 编写一个简单的demo(不能更简单): 248 | 249 | ```go 250 | package main 251 | 252 | import ( 253 | "fmt" 254 | ) 255 | 256 | func main() { 257 | 258 | c := make(chan int, 10) 259 | fmt.Println(c) 260 | } 261 | ``` 262 | 263 | 执行: 264 | 265 | ```sh 266 | [root@localhost gotest]# GODEBUG=schedtrace=1000 mygo run main.go 267 | bearluo makechan: chan=0xc000036070; elemsize=8; dataqsiz=2 268 | SCHED 0ms: gomaxprocs=16 idleprocs=13 threads=6 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] 269 | bearluo makechan: chan=0xc00010e000; elemsize=1; dataqsiz=0 270 | bearluo makechan: chan=0xc00010e060; elemsize=0; dataqsiz=0 271 | bearluo makechan: chan=0xc00010e180; elemsize=0; dataqsiz=0 272 | bearluo makechan: chan=0xc0006a8000; elemsize=1; dataqsiz=35 273 | bearluo makechan: chan=0xc0003f6660; elemsize=16; dataqsiz=2 274 | bearluo makechan: chan=0xc000226540; elemsize=16; dataqsiz=2 275 | bearluo makechan: chan=0xc0001381e0; elemsize=16; dataqsiz=2 276 | bearluo makechan: chan=0xc0005043c0; elemsize=16; dataqsiz=2 277 | bearluo makechan: chan=0xc00049c420; elemsize=16; dataqsiz=2 278 | bearluo makechan: chan=0xc000594300; elemsize=16; dataqsiz=2 279 | bearluo makechan: chan=0xc000090360; elemsize=16; dataqsiz=2 280 | bearluo makechan: chan=0xc000220000; elemsize=16; dataqsiz=2 281 | bearluo makechan: chan=0xc00075e000; elemsize=16; dataqsiz=2 282 | bearluo makechan: chan=0xc000138840; elemsize=16; dataqsiz=2 283 | bearluo makechan: chan=0xc000226780; elemsize=16; dataqsiz=2 284 | bearluo makechan: chan=0xc0003ea420; elemsize=16; dataqsiz=2 285 | bearluo makechan: chan=0xc00049d320; elemsize=16; dataqsiz=1 286 | ... 287 | ``` 288 | 289 | ## Delve 调试 290 | 291 | 目前Go语言支持GDB、LLDB和Delve几种调试器。只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go runtime代码以及汇编程序。 292 | 293 | 项目地址:https://github.com/go-delve/delve 294 | 295 | 安装: 296 | 297 | ``` 298 | go get github.com/go-delve/delve/cmd/dlv 299 | ``` 300 | 301 | 首先编写一个test.go的一个例子: 302 | 303 | ```go 304 | package main 305 | 306 | import "fmt" 307 | 308 | type A struct { 309 | test string 310 | } 311 | func main() { 312 | a := new(A) 313 | fmt.Println(a) 314 | } 315 | ``` 316 | 317 | 然后命令行进入包所在目录,然后输入`dlv debug`命令进入调试: 318 | 319 | ```powershell 320 | PS C:\document\code\test_go\src> dlv debug 321 | Type 'help' for list of commands. 322 | ``` 323 | 324 | 然后可以使用break命令在main包的main方法上设置一个断点: 325 | 326 | ```powershell 327 | (dlv) break main.main 328 | Breakpoint 1 set at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8 329 | ``` 330 | 331 | 通过breakpoints查看已经设置的所有断点: 332 | 333 | ```powershell 334 | (dlv) breakpoints 335 | Breakpoint runtime-fatal-throw at 0x4377e0 for runtime.fatalthrow() c:/software/go/src/runtime/panic.go:1162 (0) 336 | Breakpoint unrecovered-panic at 0x437860 for runtime.fatalpanic() c:/software/go/src/runtime/panic.go:1189 (0) 337 | print runtime.curg._panic.arg 338 | Breakpoint 1 at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8 (0) 339 | ``` 340 | 341 | 通过continue命令让程序运行到下一个断点处: 342 | 343 | ```powershell 344 | (dlv) continue 345 | > main.main() c:/document/code/test_go/src/test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4bd30a) 346 | 3: import "fmt" 347 | 4: 348 | 5: type A struct { 349 | 6: test string 350 | 7: } 351 | => 8: func main() { 352 | 9: a := new(A) 353 | 10: fmt.Println(a) 354 | 11: } 355 | 12: 356 | 13: 357 | ``` 358 | 359 | 通过disassemble反汇编命令查看main函数对应的汇编代码: 360 | 361 | ```powershell 362 | (dlv) disassemble 363 | TEXT main.main(SB) C:/document/code/test_go/src/test.go 364 | test.go:8 0x4bd2f0 65488b0c2528000000 mov rcx, qword ptr gs:[0x28] 365 | test.go:8 0x4bd2f9 488b8900000000 mov rcx, qword ptr [rcx] 366 | test.go:8 0x4bd300 483b6110 cmp rsp, qword ptr [rcx+0x10] 367 | test.go:8 0x4bd304 0f8697000000 jbe 0x4bd3a1 368 | => test.go:8 0x4bd30a* 4883ec78 sub rsp, 0x78 369 | test.go:8 0x4bd30e 48896c2470 mov qword ptr [rsp+0x70], rbp 370 | test.go:8 0x4bd313 488d6c2470 lea rbp, ptr [rsp+0x70] 371 | test.go:9 0x4bd318 488d0581860100 lea rax, ptr [__image_base__+874912] 372 | test.go:9 0x4bd31f 48890424 mov qword ptr [rsp], rax 373 | test.go:9 0x4bd323 e8e800f5ff call $runtime.newobject 374 | test.go:9 0x4bd328 488b442408 mov rax, qword ptr [rsp+0x8] 375 | test.go:9 0x4bd32d 4889442430 mov qword ptr [rsp+0x30], rax 376 | test.go:10 0x4bd332 4889442440 mov qword ptr [rsp+0x40], rax 377 | test.go:10 0x4bd337 0f57c0 xorps xmm0, xmm0 378 | test.go:10 0x4bd33a 0f11442448 movups xmmword ptr [rsp+0x48], xmm0 379 | test.go:10 0x4bd33f 488d442448 lea rax, ptr [rsp+0x48] 380 | test.go:10 0x4bd344 4889442438 mov qword ptr [rsp+0x38], rax 381 | test.go:10 0x4bd349 8400 test byte ptr [rax], al 382 | test.go:10 0x4bd34b 488b4c2440 mov rcx, qword ptr [rsp+0x40] 383 | test.go:10 0x4bd350 488d15099f0000 lea rdx, ptr [__image_base__+815712] 384 | test.go:10 0x4bd357 4889542448 mov qword ptr [rsp+0x48], rdx 385 | test.go:10 0x4bd35c 48894c2450 mov qword ptr [rsp+0x50], rcx 386 | test.go:10 0x4bd361 8400 test byte ptr [rax], al 387 | test.go:10 0x4bd363 eb00 jmp 0x4bd365 388 | test.go:10 0x4bd365 4889442458 mov qword ptr [rsp+0x58], rax 389 | test.go:10 0x4bd36a 48c744246001000000 mov qword ptr [rsp+0x60], 0x1 390 | test.go:10 0x4bd373 48c744246801000000 mov qword ptr [rsp+0x68], 0x1 391 | test.go:10 0x4bd37c 48890424 mov qword ptr [rsp], rax 392 | test.go:10 0x4bd380 48c744240801000000 mov qword ptr [rsp+0x8], 0x1 393 | test.go:10 0x4bd389 48c744241001000000 mov qword ptr [rsp+0x10], 0x1 394 | test.go:10 0x4bd392 e869a0ffff call $fmt.Println 395 | test.go:11 0x4bd397 488b6c2470 mov rbp, qword ptr [rsp+0x70] 396 | test.go:11 0x4bd39c 4883c478 add rsp, 0x78 397 | test.go:11 0x4bd3a0 c3 ret 398 | test.go:8 0x4bd3a1 e82a50faff call $runtime.morestack_noctxt 399 | .:0 0x4bd3a6 e945ffffff jmp $main.main 400 | ``` 401 | 402 | 现在我们可以使用break断点到runtime.newobject函数的调用上: 403 | 404 | ```powershell 405 | (dlv) break runtime.newobject 406 | Breakpoint 2 set at 0x40d426 for runtime.newobject() c:/software/go/src/runtime/malloc.go:1164 407 | ``` 408 | 409 | 输入continue跳到断点的位置: 410 | 411 | ```powershell 412 | (dlv) continue 413 | > runtime.newobject() c:/software/go/src/runtime/malloc.go:1164 (hits goroutine(1):1 total:1) (PC: 0x40d426) 414 | Warning: debugging optimized function 415 | 1159: } 416 | 1160: 417 | 1161: // implementation of new builtin 418 | 1162: // compiler (both frontend and SSA backend) knows the signature 419 | 1163: // of this function 420 | =>1164: func newobject(typ *_type) unsafe.Pointer { 421 | 1165: return mallocgc(typ.size, typ, true) 422 | 1166: } 423 | 1167: 424 | 1168: //go:linkname reflect_unsafe_New reflect.unsafe_New 425 | 1169: func reflect_unsafe_New(typ *_type) unsafe.Pointer { 426 | ``` 427 | 428 | print命令来查看typ的数据: 429 | 430 | ```powershell 431 | (dlv) print typ 432 | *runtime._type {size: 16, ptrdata: 8, hash: 875453117, tflag: tflagUncommon|tflagExtraStar|tflagNamed (7), align: 8, fieldAlign: 8, kind: 25, equal: runtime.strequal, gcdata: *1, str: 5418, ptrToThis: 37472} 433 | ``` 434 | 435 | 可以看到这里打印的size是16bytes,因为我们A结构体里面就一个string类型的field。 436 | 437 | 进入到mallocgc方法后,通过args和locals命令查看函数的参数和局部变量: 438 | 439 | ```sh 440 | (dlv) args 441 | size = (unreadable could not find loclist entry at 0x8b40 for address 0x40ca73) 442 | typ = (*runtime._type)(0x4d59a0) 443 | needzero = true 444 | ~r3 = (unreadable empty OP stack) 445 | (dlv) locals 446 | (no locals) 447 | ``` 448 | 449 | ## Reference 450 | 451 | Installing Go from source https://golang.org/doc/install/source 452 | 453 | Scheduler Tracing In Go https://www.ardanlabs.com/blog/2015/02/scheduler-tracing-in-go.html 454 | 455 | GODEBUG https://golang.org/pkg/runtime/ 456 | 457 | 用 GODEBUG 看调度跟踪 https://eddycjy.com/posts/go/tools/2019-08-19-godebug-sched/ 458 | 459 | -------------------------------------------------------------------------------- /深入istio/1.深入Istio.md: -------------------------------------------------------------------------------- 1 | # 1.深入Istio源码:Sidecar注入实现原理 2 | 3 | ![80614165_p0_master1200](1.深入Istio/80614165_p0_master1200.jpg) 4 | 5 | > 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 6 | > 7 | > 本文使用的Istio源码是 release 1.5。 8 | > 9 | 10 | 这篇文章打算讲一下sidecar,我在刚学习Istio的时候会有一些疑惑,sidecar是如何做到无感知的注入的,很多学习资料都没有详细去讲这部分的内容,下面打算解析一下。 11 | 12 | ## Sidecar 介绍 13 | 14 | 在Sidecar部署方式中会为每个应用的容器部署一个伴生容器。对于Istio,Sidecar接管进出应用程序容器的所有网络流量。 15 | 16 | 使用 Sidecar 模式部署服务网格时,无需在节点上运行代理,但是集群中将运行多个相同的 Sidecar 副本。在 Kubernetes 的 Pod 中,在原有的应用容器旁边运行一个 Sidecar 容器,可以理解为两个容器共享存储、网络等资源,可以广义的将这个注入了 Sidecar 容器的 Pod 理解为一台主机,两个容器共享主机资源。 17 | 18 | ### Sidecar 注入过程 19 | 20 | 注入 Sidecar的时候会在生成pod的时候附加上两个容器:istio-init、istio-proxy。istio-init这个容器从名字上看也可以知道它属于k8s中的Init Containers,主要用于设置iptables规则,让出入流量都转由 Sidecar 进行处理。istio-proxy是基于Envoy实现的一个网络代理容器,是真正的Sidecar,应用的流量会被重定向进入或流出Sidecar。 21 | 22 | 我们在使用Sidecar自动注入的时候只需要给对应的应用部署的命名空间打个istio-injection=enabled标签,这个命名空间中新建的任何 Pod 都会被 Istio 注入 Sidecar。 23 | 24 | 应用部署后我们可以通过kubectl describe查看pod内的容器: 25 | 26 | ```shell 27 | [root@localhost ~]# kubectl describe pod details-v1-6c9f8bcbcb-shltm 28 | 29 | Name: details-v1-6c9f8bcbcb-shltm 30 | Namespace: default 31 | ... 32 | Labels: app=details 33 | pod-template-hash=6c9f8bcbcb 34 | security.istio.io/tlsMode=istio 35 | service.istio.io/canonical-name=details 36 | service.istio.io/canonical-revision=v1 37 | version=v1 38 | Annotations: sidecar.istio.io/status: 39 | {"version":"3bc68d1f27d8b6b9bf1cb3e9904f5d5f8c2ecab1c93d933fbb3d0db76fae2633","initContainers":["istio-init"],"containers":["istio-proxy"]... 40 | Status: Running 41 | IP: 172.20.0.14 42 | IPs: 43 | IP: 172.20.0.14 44 | Controlled By: ReplicaSet/details-v1-6c9f8bcbcb 45 | Init Containers: 46 | istio-init: 47 | Container ID: docker://6d14ccc83bd119236bf8fda13f6799609c87891be9b2c5af7cbf7d8c913ce17e 48 | Image: docker.io/istio/proxyv2:1.5.10 49 | Image ID: docker-pullable://istio/proxyv2@sha256:abbe8ad6d50474814f1aa9316dafc2401fbba89175638446f01afc36b5a37919 50 | ... 51 | Ready: True 52 | Restart Count: 0 53 | ... 54 | Containers: 55 | details: 56 | Container ID: docker://ed216429216ea1b8a1ba20960590edb7322557467c38cceff3c3e847bcff0a14 57 | Image: docker.io/istio/examples-bookinfo-details-v1:1.15.1 58 | Image ID: docker-pullable://istio/examples-bookinfo-details-v1@sha256:344b1c18703ab1e51aa6d698f459c95ea734f8317d779189f4638de7a00e61ae 59 | ... 60 | istio-proxy: 61 | Container ID: docker://a3862cc8f53198c8f86a911089e73e00f4cc4aa02eea05aaeb0bd267a8e98482 62 | Image: docker.io/istio/proxyv2:1.5.10 63 | Image ID: docker-pullable://istio/proxyv2@sha256:abbe8ad6d50474814f1aa9316dafc2401fbba89175638446f01afc36b5a37919 64 | ... 65 | Ready: True 66 | ``` 67 | 68 | details-v1-6c9f8bcbcb-shltm这个应用是我们在上篇文章中创建的一个details服务,里面有istio-init、istio-proxy、details这三个container。 69 | 70 | ## Sidecar 注入原理 71 | 72 | Sidecar 注入主要是依托k8s的准入控制器Admission Controller来实现的。 73 | 74 | 准入控制器会拦截 Kubernetes API Server 收到的请求,拦截发生在认证和鉴权完成之后,对象进行持久化之前。可以定义两种类型的 Admission webhook:Validating 和 Mutating。Validating 类型的 Webhook 可以根据自定义的准入策略决定是否拒绝请求;Mutating 类型的 Webhook 可以根据自定义配置来对请求进行编辑。 75 | 76 | 我们可以看看配置详情: 77 | 78 | ```shell 79 | [root@localhost ~]# kubectl get mutatingwebhookconfiguration istio-sidecar-injector -o yaml 80 | 81 | apiVersion: admissionregistration.k8s.io/v1 82 | kind: MutatingWebhookConfiguration 83 | metadata: 84 | annotations: 85 | ... 86 | creationTimestamp: "2020-10-18T08:22:01Z" 87 | generation: 2 88 | labels: 89 | app: sidecar-injector 90 | operator.istio.io/component: Pilot 91 | operator.istio.io/managed: Reconcile 92 | operator.istio.io/version: 1.5.10 93 | release: istio 94 | ... 95 | webhooks: 96 | - admissionReviewVersions: 97 | - v1beta1 98 | clientConfig: 99 | caBundle: ... 100 | service: 101 | name: istiod 102 | namespace: istio-system 103 | path: /inject 104 | port: 443 105 | failurePolicy: Fail 106 | matchPolicy: Exact 107 | name: sidecar-injector.istio.io 108 | namespaceSelector: 109 | matchLabels: 110 | istio-injection: enabled 111 | rules: 112 | - apiGroups: 113 | - "" 114 | apiVersions: 115 | - v1 116 | operations: 117 | - CREATE 118 | resources: 119 | - pods 120 | scope: '*' 121 | ... 122 | ``` 123 | 124 | 这里有一个namespaceSelector,match的标签是istio-injection: enabled的命名空间,请求规则rules是CREATE,表示匹配所有pod的创建请求。当apiserver收到一个符合规则的请求时,apiserver会给 Webhook 服务发送一个准入审核的请求,在上面的配置中webhook指定的是一个叫istiod的service。 125 | 126 | ```shell 127 | [root@localhost ~]# kubectl get svc --namespace=istio-system | grep istiod 128 | istiod ClusterIP 10.68.222.38 15012/TCP,443/TCP 129 | 32h 130 | ``` 131 | 132 | 133 | 134 | 通常Sidecar注入由以下步骤完成: 135 | 136 | 1. 解析Webhook REST请求,将AdmissionReview原始数据反序列化; 137 | 2. 解析pod,将AdmissionReview中的AdmissionRequest反序列化; 138 | 3. 利用Pod及网格配置渲染Sidecar配置模板; 139 | 4. 利用Pod及渲染后的模板创建Json Patch; 140 | 5. 构造AdmissionResponse; 141 | 6. 构造AdmissionReview,将其发给apiserver; 142 | 143 | 源码流程差不多是这个样子: 144 | 145 | ![image-20201107174326312](1.深入Istio/image-20201107174326312.png) 146 | 147 | 下面我们来看看源码。 148 | 149 | 源码位置:pkg/kube/inject/webhook.go 150 | 151 | ```go 152 | func NewWebhook(p WebhookParameters) (*Webhook, error) { 153 | wh := &Webhook{ 154 | ... 155 | } 156 | ... 157 | if p.Mux != nil { 158 | p.Mux.HandleFunc("/inject", wh.serveInject) 159 | mux = p.Mux 160 | } else { 161 | wh.server = &http.Server{ 162 | Addr: fmt.Sprintf(":%v", p.Port), 163 | TLSConfig: &tls.Config{GetCertificate: wh.getCert}, 164 | } 165 | mux = http.NewServeMux() 166 | mux.HandleFunc("/inject", wh.serveInject) 167 | wh.server.Handler = mux 168 | } 169 | ... 170 | } 171 | ``` 172 | 173 | 在初始化Webhook实例的时候会注册/inject对应的处理器,也就是当apiserver回调/inject请求的时候会调用到serveInject方法中。 174 | 175 | 然后我们进入到serveInject方法中: 176 | 177 | 文件位置:pkg/kube/inject/webhook.go 178 | 179 | ```go 180 | func (wh *Webhook) serveInject(w http.ResponseWriter, r *http.Request) { 181 | totalInjections.Increment() 182 | var body []byte 183 | if r.Body != nil { 184 | //读取请求体 185 | if data, err := ioutil.ReadAll(r.Body); err == nil { 186 | body = data 187 | } 188 | } 189 | ... 190 | var reviewResponse *v1beta1.AdmissionResponse 191 | ar := v1beta1.AdmissionReview{} 192 | //解码请求体 193 | if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { 194 | handleError(fmt.Sprintf("Could not decode body: %v", err)) 195 | reviewResponse = toAdmissionResponse(err) 196 | } else { 197 | //解码成功调用inject方法,并传入AdmissionReview 198 | reviewResponse = wh.inject(&ar) 199 | } 200 | //构建AdmissionReview作为参数返回给调用方 201 | response := v1beta1.AdmissionReview{} 202 | if reviewResponse != nil { 203 | response.Response = reviewResponse 204 | if ar.Request != nil { 205 | response.Response.UID = ar.Request.UID 206 | } 207 | } 208 | 209 | resp, err := json.Marshal(response) 210 | if err != nil { 211 | log.Errorf("Could not encode response: %v", err) 212 | http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) 213 | } 214 | if _, err := w.Write(resp); err != nil { 215 | log.Errorf("Could not write response: %v", err) 216 | http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) 217 | } 218 | } 219 | ``` 220 | 221 | 这个方法很简单,主要就是读取请求体并解码,然后调用inject方法,构建AdmissionReview作为参数返回给调用方。 222 | 223 | 主要逻辑从这里可以看出都在inject方法里面,下面看看这个方法: 224 | 225 | ```go 226 | func (wh *Webhook) inject(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { 227 | req := ar.Request 228 | var pod corev1.Pod 229 | //json反序列化请求数据 230 | if err := json.Unmarshal(req.Object.Raw, &pod); err != nil { 231 | handleError(fmt.Sprintf("Could not unmarshal raw object: %v %s", err, 232 | string(req.Object.Raw))) 233 | return toAdmissionResponse(err) 234 | } 235 | 236 | ... 237 | //封装模板数据 238 | spec, iStatus, err := InjectionData(wh.Config.Template, wh.valuesConfig, wh.sidecarTemplateVersion, typeMetadata, deployMeta, &pod.Spec, &pod.ObjectMeta, wh.meshConfig.DefaultConfig, wh.meshConfig) // nolint: lll 239 | if err != nil { 240 | handleError(fmt.Sprintf("Injection data: err=%v spec=%v\n", err, iStatus)) 241 | return toAdmissionResponse(err) 242 | } 243 | ... 244 | //将需要注入的有istio-init/istio-proxy container封装成patch操作 245 | //具体可以看这里:https://kubernetes.io/zh/docs/reference/access-authn-authz/extensible-admission-controllers/#response 246 | patchBytes, err := createPatch(&pod, injectionStatus(&pod), annotations, spec, deployMeta.Name) 247 | if err != nil { 248 | handleError(fmt.Sprintf("AdmissionResponse: err=%v spec=%v\n", err, spec)) 249 | return toAdmissionResponse(err) 250 | } 251 | 252 | log.Debugf("AdmissionResponse: patch=%v\n", string(patchBytes)) 253 | //将需要patch的配置封装成AdmissionResponse返回 254 | reviewResponse := v1beta1.AdmissionResponse{ 255 | Allowed: true, 256 | Patch: patchBytes, 257 | PatchType: func() *v1beta1.PatchType { 258 | pt := v1beta1.PatchTypeJSONPatch 259 | return &pt 260 | }(), 261 | } 262 | totalSuccessfulInjections.Increment() 263 | return &reviewResponse 264 | } 265 | ``` 266 | 267 | inject方法逻辑主要分为以下几个步骤: 268 | 269 | 1. json反序列化请求数据到pod中; 270 | 2. 调用InjectionData根据模板封装数据,主要是构造istio-init、istio-proxy等容器配置; 271 | 3. 调用createPatch方法将模板数据转化成json形式,到时候在创建容器的时候会patch到创建容器的配置中,具体可以看这里:https://kubernetes.io/zh/docs/reference/access-authn-authz/extensible-admission-controllers/#response 272 | 4. 最后将数据封装成AdmissionResponse返回; 273 | 274 | ## 总结 275 | 276 | 本篇文章重点讲解Sidecar容器注入实现原理,通过使用k8s的准入控制器来做到在每个新建的pod里面都无感知的创建sidecar做流量托管。 277 | 278 | ## Reference 279 | 280 | https://github.com/istio/istio.io/blob/release-1.1/content/blog/2019/data-plane-setup/index.md 281 | 282 | https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/ 283 | 284 | https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/ 285 | 286 | https://jimmysong.io/blog/envoy-sidecar-injection-in-istio-service-mesh-deep-dive/ -------------------------------------------------------------------------------- /深入istio/1.深入Istio/80614165_p0_master1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/1.深入Istio/80614165_p0_master1200.jpg -------------------------------------------------------------------------------- /深入istio/1.深入Istio/image-20201107174326312.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/1.深入Istio/image-20201107174326312.png -------------------------------------------------------------------------------- /深入istio/2.深入Istio.md: -------------------------------------------------------------------------------- 1 | # 2.深入Istio:VirtualService虚拟服务路由 2 | 3 | 4 | 5 | ## 概述 6 | 7 | 我在[从一个例子入手Istio](https://www.luozhiyun.com/archives/393)这篇文章中讲述了一个Bookinfo 示例,在这个示例中通过部署bookinfo-gateway.yaml来实现让应用程序可以从外部访问 k8s 集群中的Bookinfo 应用。 8 | 9 | 我们先来看看bookinfo-gateway.yaml里面做了什么: 10 | 11 | ```sh 12 | $ cat samples/bookinfo/networking/bookinfo-gateway.yaml 13 | 14 | apiVersion: networking.istio.io/v1alpha3 15 | kind: Gateway 16 | metadata: 17 | name: bookinfo-gateway 18 | spec: 19 | selector: 20 | istio: ingressgateway # use istio default controller 21 | servers: 22 | - port: 23 | number: 80 24 | name: http 25 | protocol: HTTP 26 | hosts: 27 | - "*" 28 | --- 29 | apiVersion: networking.istio.io/v1alpha3 30 | kind: VirtualService 31 | metadata: 32 | name: bookinfo 33 | spec: 34 | hosts: 35 | - "*" 36 | gateways: 37 | - bookinfo-gateway 38 | http: 39 | - match: 40 | - uri: 41 | exact: /productpage 42 | - uri: 43 | prefix: /static 44 | - uri: 45 | exact: /login 46 | - uri: 47 | exact: /logout 48 | - uri: 49 | prefix: /api/v1/products 50 | route: 51 | - destination: 52 | host: productpage 53 | port: 54 | number: 9080 55 | ``` 56 | 57 | 这里面定义了一个VirtualService和一个Gateway。在上面的VirtualService 中定义了对特定目标服务的一组流量规则。从上面的字段名也可以很清楚的看到,VirtualService匹配了哪些url,以及对应的目的地destination的host与port是什么。 58 | 59 | VirtualService主要的功能就是将满足条件的流量都转发到对应的服务后端,这个服务后端可以是一个服务,也可以是在DestinationRule中定义的服务子集。 60 | 61 | ## 设计 62 | 63 | ### VirtualService的定义 64 | 65 | 下面我们看看VirtualService的定义: 66 | 67 | ```go 68 | type VirtualService struct { 69 | Hosts []string `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"` 70 | Gateways []string `protobuf:"bytes,2,rep,name=gateways,proto3" json:"gateways,omitempty"` 71 | Http []*HTTPRoute `protobuf:"bytes,3,rep,name=http,proto3" json:"http,omitempty"` 72 | Tls []*TLSRoute `protobuf:"bytes,5,rep,name=tls,proto3" json:"tls,omitempty"` 73 | Tcp []*TCPRoute `protobuf:"bytes,4,rep,name=tcp,proto3" json:"tcp,omitempty"` 74 | ExportTo []string `protobuf:"bytes,6,rep,name=export_to,json=exportTo,proto3" json:"export_to,omitempty"` 75 | } 76 | ``` 77 | 78 | 透过上面的定义,我们可以很清楚的看到VirtualService的路由规则是通过Hosts、Gateways、HTTPRoute、TLSRoute、TCPRoute来设置的。 79 | 80 | * Hosts:表示流量发送的目标,用于匹配访问地址,可以是一个DNS名称或IP地址。DNS名称可以使用通配符前缀,也可以只使用短域名或全限定域名FQDN。 81 | * Gateways:表示应用这些流量规则的Gateways。VirtualService描述的规则可以作用到网格里的Sidecar和入口处的Gateway,表示将路由规则应用于网格内的访问还是网格外经过Gateway的访问。 82 | * Http:是一个HTTPRoute类似的路由集合,用于处理HTTP的流量。 83 | * Tls:是TLSRoute类型的路由集合,用于处理非终结的TLS和HTTPS的流量。 84 | * Tcp:是一个TCPRoute类型的路由集合,用于处理TCP的流量,应用于所有其他非HTTP和TLS端口的流量。 85 | * ExportTo:用于控制跨命名空间的可见性,可以用来控制在一个命名空间下定义的VirtualService是否可以被其他命名空间下的Sidecar和Gateway使用。 86 | 87 | ### HTTPRoute规则 88 | 89 | 下面来专门看看HTTPRoute,Http是当前最通用的协议,也是Istio上支持最完整的一种协议。 90 | 91 | 下面看看HTTPRoute的定义: 92 | 93 | ```go 94 | type HTTPRoute struct { 95 | Name string `protobuf:"bytes,17,opt,name=name,proto3" json:"name,omitempty"` 96 | Match []*HTTPMatchRequest `protobuf:"bytes,1,rep,name=match,proto3" json:"match,omitempty"` 97 | Route []*HTTPRouteDestination `protobuf:"bytes,2,rep,name=route,proto3" json:"route,omitempty"` 98 | Redirect *HTTPRedirect `protobuf:"bytes,3,opt,name=redirect,proto3" json:"redirect,omitempty"` 99 | Rewrite *HTTPRewrite `protobuf:"bytes,4,opt,name=rewrite,proto3" json:"rewrite,omitempty"` 100 | Timeout *types.Duration `protobuf:"bytes,6,opt,name=timeout,proto3" json:"timeout,omitempty"` 101 | Retries *HTTPRetry `protobuf:"bytes,7,opt,name=retries,proto3" json:"retries,omitempty"` 102 | Fault *HTTPFaultInjection `protobuf:"bytes,8,opt,name=fault,proto3" json:"fault,omitempty"` 103 | Mirror *Destination `protobuf:"bytes,9,opt,name=mirror,proto3" json:"mirror,omitempty"` 104 | CorsPolicy *CorsPolicy `protobuf:"bytes,10,opt,name=cors_policy,json=corsPolicy,proto3" json:"cors_policy,omitempty"` 105 | Headers *Headers `protobuf:"bytes,16,opt,name=headers,proto3" json:"headers,omitempty"` 106 | } 107 | ``` 108 | 109 | 单单只看这个结构体的定义就大概可以知道,HTTPRoute可以实现:重定向(HTTPRedirect)、重写(HTTPRewrite)、重试(Retries)、故障注入(HTTPFaultInjection)、流量镜像(Mirror)、跨站(CorsPolicy)等功能。 110 | 111 | ![image-20201115153309906](2.深入Istio/image-20201115153309906.png) 112 | 113 | #### HTTPMatchRequest 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | virtualService 124 | 125 | 文件位置:galley/pkg/config/processor/transforms/ingress/virtualService.go 126 | 127 | 128 | 129 | ## Reference 130 | 131 | https://istio.io/latest/docs/reference/config/networking/virtual-service/ 132 | 133 | https://www.servicemesher.com/istio-handbook/practice/traffic-control.html 134 | 135 | https://istio.io/latest/zh/docs/ops/diagnostic-tools/proxy-cmd/ 136 | 137 | https://istio.io/v1.5/docs/ops/diagnostic-tools/proxy-cmd/ -------------------------------------------------------------------------------- /深入istio/2.深入Istio/image-20201115153309906.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/2.深入Istio/image-20201115153309906.png -------------------------------------------------------------------------------- /深入istio/2.深入Istio:Pilot服务发现.md: -------------------------------------------------------------------------------- 1 | # 2.深入Istio源码:Pilot服务发现 2 | 3 | > 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 4 | > 5 | > 本文使用的Istio源码是 release 1.5。 6 | 7 | ## 介绍 8 | 9 | pilot-discovery是在Pilot中的核心服务,在Pilot中名为pilot-discovery,主要功能是从注册中心(如 kubernetes 或者 consul)获取信息并汇集,从 Kubernetes API Server 中获取流量规则,并将服务信息和流量规则转化为数据面可以理解的格式,通过标准的数据面 API 下发到网格中的各个SideCar中。 10 | 11 | pilot-discovery包含了服务发现、配置规则发现、xDS配置下发。总体上打算分三篇来进行讲解,这一篇主要看看服务发现部分的实现。文章中有涉及xDS协议的一些东西,大家可以看看这篇文章:[深入解读Service Mesh背后的技术细节](https://www.cnblogs.com/163yun/p/8962278.html)。 12 | 13 | ![istio](2.深入Istio:Pilot服务发现/istio.png) 14 | 15 | Pilot服务发现指通过监听底层平台的服务注册中心来缓存Istio服务模型,并且监视服务模型的变化,再服务模型更新时触发相关事件回调处理函数的执行。 16 | 17 | ## 服务发现工作机制 18 | 19 | ### Pilot初始化 20 | 21 | ```go 22 | discoveryCmd = &cobra.Command{ 23 | Use: "discovery", 24 | Short: "Start Istio proxy discovery service.", 25 | Args: cobra.ExactArgs(0), 26 | RunE: func(c *cobra.Command, args []string) error { 27 | ... 28 | //日志配置 29 | if err := log.Configure(loggingOptions); err != nil { 30 | return err 31 | } 32 | ... 33 | stop := make(chan struct{}) 34 | 35 | // 创建xDs服务器 36 | discoveryServer, err := bootstrap.NewServer(&serverArgs) 37 | if err != nil { 38 | return fmt.Errorf("failed to create discovery service: %v", err) 39 | } 40 | // 启动服务器 41 | if err := discoveryServer.Start(stop); err != nil { 42 | return fmt.Errorf("failed to start discovery service: %v", err) 43 | } 44 | //等待进程推出 45 | cmd.WaitSignal(stop) 46 | discoveryServer.WaitUntilCompletion() 47 | return nil 48 | }, 49 | } 50 | ``` 51 | 52 | Pilot服务在初始化的时候首先会初始化日志配置,然后创建xDs服务器,这里的xDs指的是x Discovery Service的意思,x代表了一系列的组件如:Cluster、Endpoint、Listener、Route 等。 53 | 54 | ```go 55 | func NewServer(args *PilotArgs) (*Server, error) { 56 | 57 | args.Default() 58 | e := &model.Environment{ 59 | ServiceDiscovery: aggregate.NewController(), 60 | PushContext: model.NewPushContext(), 61 | } 62 | 63 | s := &Server{ 64 | basePort: args.BasePort, 65 | clusterID: getClusterID(args), 66 | environment: e, 67 | EnvoyXdsServer: envoyv2.NewDiscoveryServer(e, args.Plugins), 68 | forceStop: args.ForceStop, 69 | mux: http.NewServeMux(), 70 | } 71 | 72 | // 初始化处理Istio Config的控制器 73 | if err := s.initConfigController(args); err != nil { 74 | return nil, fmt.Errorf("config controller: %v", err) 75 | } 76 | // 初始化处理Service Discovery的控制器 77 | if err := s.initServiceControllers(args); err != nil { 78 | return nil, fmt.Errorf("service controllers: %v", err) 79 | } 80 | ... 81 | //初始化xDS服务端 82 | if err := s.initDiscoveryService(args); err != nil { 83 | return nil, fmt.Errorf("discovery service: %v", err) 84 | } 85 | ... 86 | // Webhook 回调服务 87 | if err := s.initHTTPSWebhookServer(args); err != nil { 88 | return nil, fmt.Errorf("injectionWebhook server: %v", err) 89 | } 90 | //sidecar注入相关 91 | if err := s.initSidecarInjector(args); err != nil { 92 | return nil, fmt.Errorf("sidecar injector: %v", err) 93 | } 94 | 95 | ... 96 | return s, nil 97 | } 98 | ``` 99 | 100 | NewServer方法里面初始化了很多模块,这里挑相关的看看initConfigController是和配置服务相关的,我们之后再看,这里我们主要看initServiceControllers。 101 | 102 | ### ServiceControllers 103 | 104 | 服务发现的主要逻辑在Pilot中由ServiceController(服务控制器)实现,通过监听底层平台的服务注册中心来缓存Istio服务模型,并监视服务模型的变化,在服务模型更新时触发相关事件回调处理函数的执行。 105 | 106 | ![serviceController](深入Istio/serviceController.png) 107 | 108 | #### 初始化 109 | 110 | Controller的初始化执行流程很简单,这里用一张图来描述,initServiceControllers方法最后会调用到NewController方法来进行初始化。 111 | 112 | ![Group 1](深入Istio/Group 1.png) 113 | 114 | ```go 115 | func NewController(client kubernetes.Interface, options Options) *Controller { 116 | log.Infof("Service controller watching namespace %q for services, endpoints, nodes and pods, refresh %s", 117 | options.WatchedNamespace, options.ResyncPeriod) 118 | 119 | // The queue requires a time duration for a retry delay after a handler error 120 | // 初始化Controller 121 | c := &Controller{ 122 | domainSuffix: options.DomainSuffix, 123 | client: client, 124 | //控制器任务队列 125 | queue: queue.NewQueue(1 * time.Second), 126 | clusterID: options.ClusterID, 127 | xdsUpdater: options.XDSUpdater, 128 | servicesMap: make(map[host.Name]*model.Service), 129 | externalNameSvcInstanceMap: make(map[host.Name][]*model.ServiceInstance), 130 | networksWatcher: options.NetworksWatcher, 131 | metrics: options.Metrics, 132 | } 133 | //获取informer 134 | sharedInformers := informers.NewSharedInformerFactoryWithOptions(client, options.ResyncPeriod, informers.WithNamespace(options.WatchedNamespace)) 135 | //注册 informer处理器 136 | c.services = sharedInformers.Core().V1().Services().Informer() 137 | //Services Handler 138 | registerHandlers(c.services, c.queue, "Services", c.onServiceEvent) 139 | //endpoints Handler 140 | switch options.EndpointMode { 141 | case EndpointsOnly: 142 | c.endpoints = newEndpointsController(c, sharedInformers) 143 | case EndpointSliceOnly: 144 | c.endpoints = newEndpointSliceController(c, sharedInformers) 145 | } 146 | //Nodes Handler 147 | c.nodes = sharedInformers.Core().V1().Nodes().Informer() 148 | registerHandlers(c.nodes, c.queue, "Nodes", c.onNodeEvent) 149 | 150 | podInformer := sharedInformers.Core().V1().Pods().Informer() 151 | c.pods = newPodCache(podInformer, c) 152 | //Pods Handler 153 | registerHandlers(podInformer, c.queue, "Pods", c.pods.onEvent) 154 | 155 | return c 156 | } 157 | ``` 158 | 159 | NewController方法里面首先是初始化Controller,然后获取informer后分别注册Services Handler、endpoints Handler、Nodes Handler、Pods Handler。 160 | 161 | 核心功能就是监听k8s相关资源(Service、Endpoint、Pod、Node)的更新事件,执行相应的事件处理回调函数。 162 | 163 | 这里的Controller结构体实现了Controller接口: 164 | 165 | ```go 166 | type Controller interface { 167 | // AppendServiceHandler notifies about changes to the service catalog. 168 | AppendServiceHandler(f func(*Service, Event)) error 169 | 170 | // AppendInstanceHandler notifies about changes to the service instances 171 | // for a service. 172 | AppendInstanceHandler(f func(*ServiceInstance, Event)) error 173 | 174 | // Run until a signal is received 175 | Run(stop <-chan struct{}) 176 | } 177 | ``` 178 | 179 | 再注册完毕后会调用其Run方法异步执行。 180 | 181 | ```go 182 | //异步调用Run方法 183 | go serviceControllers.Run(stop) 184 | //run方法里面会遍历GetRegistries列表,并异步执行其Run方法 185 | func (c *Controller) Run(stop <-chan struct{}) { 186 | 187 | for _, r := range c.GetRegistries() { 188 | go r.Run(stop) 189 | } 190 | 191 | <-stop 192 | log.Info("Registry Aggregator terminated") 193 | } 194 | ``` 195 | 196 | 到这里ServiceController为四种资源分别创建了一个监听器,用于监听K8s的资源更新,并注册EventHandler。 197 | 198 | ![Group 1](深入Istio/Group 1-1606017533527.png) 199 | 200 | #### Service处理器 201 | 202 | ```go 203 | func (c *Controller) onServiceEvent(curr interface{}, event model.Event) error { 204 | if err := c.checkReadyForEvents(); err != nil { 205 | return err 206 | } 207 | 208 | svc, ok := curr.(*v1.Service) 209 | if !ok { 210 | tombstone, ok := curr.(cache.DeletedFinalStateUnknown) 211 | if !ok { 212 | log.Errorf("Couldn't get object from tombstone %#v", curr) 213 | return nil 214 | } 215 | svc, ok = tombstone.Obj.(*v1.Service) 216 | if !ok { 217 | log.Errorf("Tombstone contained object that is not a service %#v", curr) 218 | return nil 219 | } 220 | } 221 | 222 | log.Debugf("Handle event %s for service %s in namespace %s", event, svc.Name, svc.Namespace) 223 | //将k8s service 转换成 istio service 224 | svcConv := kube.ConvertService(*svc, c.domainSuffix, c.clusterID) 225 | //根据事件类型处理事件 226 | switch event { 227 | //删除事件 228 | case model.EventDelete: 229 | c.Lock() 230 | delete(c.servicesMap, svcConv.Hostname) 231 | delete(c.externalNameSvcInstanceMap, svcConv.Hostname) 232 | c.Unlock() 233 | // EDS needs to just know when service is deleted. 234 | //更新服务缓存 235 | c.xdsUpdater.SvcUpdate(c.clusterID, svc.Name, svc.Namespace, event) 236 | default: 237 | // instance conversion is only required when service is added/updated. 238 | instances := kube.ExternalNameServiceInstances(*svc, svcConv) 239 | c.Lock() 240 | c.servicesMap[svcConv.Hostname] = svcConv 241 | if instances == nil { 242 | delete(c.externalNameSvcInstanceMap, svcConv.Hostname) 243 | } else { 244 | c.externalNameSvcInstanceMap[svcConv.Hostname] = instances 245 | } 246 | c.Unlock() 247 | //更新服务缓存 248 | c.xdsUpdater.SvcUpdate(c.clusterID, svc.Name, svc.Namespace, event) 249 | } 250 | 251 | // Notify service handlers. 252 | // 触发XDS事件处理器 253 | for _, f := range c.serviceHandlers { 254 | f(svcConv, event) 255 | } 256 | 257 | return nil 258 | } 259 | ``` 260 | 261 | Service事件处理器会将根据事件的类型更新缓存,然后调用serviceHandlers的事件处理器进行回调。serviceHandlers事件处理器是在初始化DiscoveryService的时候设置的。 262 | 263 | ```go 264 | serviceHandler := func(svc *model.Service, _ model.Event) { 265 | pushReq := &model.PushRequest{ 266 | Full: true, 267 | NamespacesUpdated: map[string]struct{}{svc.Attributes.Namespace: {}}, 268 | ConfigTypesUpdated: map[resource.GroupVersionKind]struct{}{collections.IstioNetworkingV1Alpha3Serviceentries.Resource().GroupVersionKind(): {}}, 269 | Reason: []model.TriggerReason{model.ServiceUpdate}, 270 | } 271 | //配置更新 272 | s.EnvoyXdsServer.ConfigUpdate(pushReq) 273 | } 274 | ``` 275 | 276 | #### Endpoint处理器 277 | 278 | Endpoint处理器会在调用newEndpointsController创建endpointsController的时候进行注册 279 | 280 | ```go 281 | func newEndpointsController(c *Controller, sharedInformers informers.SharedInformerFactory) *endpointsController { 282 | informer := sharedInformers.Core().V1().Endpoints().Informer() 283 | out := &endpointsController{ 284 | kubeEndpoints: kubeEndpoints{ 285 | c: c, 286 | informer: informer, 287 | }, 288 | } 289 | //注册处理器 290 | out.registerEndpointsHandler() 291 | return out 292 | } 293 | ``` 294 | 295 | 在回调的时候会调用到endpointsController的onEvent方法: 296 | 297 | ```go 298 | func (e *endpointsController) onEvent(curr interface{}, event model.Event) error { 299 | ... 300 | return e.handleEvent(ep.Name, ep.Namespace, event, curr, func(obj interface{}, event model.Event) { 301 | ep := obj.(*v1.Endpoints) 302 | //EDS更新处理 303 | e.c.updateEDS(ep, event) 304 | }) 305 | } 306 | ``` 307 | 308 | 这里会调用updateEDS进行EDS(Endpoint Discovery service)更新处理。 309 | 310 | ```go 311 | func (c *Controller) updateEDS(ep *v1.Endpoints, event model.Event) { 312 | hostname := kube.ServiceHostname(ep.Name, ep.Namespace, c.domainSuffix) 313 | 314 | endpoints := make([]*model.IstioEndpoint, 0) 315 | if event != model.EventDelete { 316 | for _, ss := range ep.Subsets { 317 | for _, ea := range ss.Addresses { 318 | //获取Endpoint对应的Pod实例 319 | pod := c.pods.getPodByIP(ea.IP) 320 | ... 321 | // 将Endpoint转换成Istio模型IstioEndpoint 322 | for _, port := range ss.Ports { 323 | endpoints = append(endpoints, &model.IstioEndpoint{ 324 | Address: ea.IP, 325 | EndpointPort: uint32(port.Port), 326 | ServicePortName: port.Name, 327 | Labels: labelMap, 328 | UID: uid, 329 | ServiceAccount: sa, 330 | Network: c.endpointNetwork(ea.IP), 331 | Locality: locality, 332 | Attributes: model.ServiceAttributes{Name: ep.Name, Namespace: ep.Namespace}, 333 | TLSMode: tlsMode, 334 | }) 335 | } 336 | } 337 | } 338 | } 339 | //使用xdsUpdater更新EDS 340 | _ = c.xdsUpdater.EDSUpdate(c.clusterID, string(hostname), ep.Namespace, endpoints) 341 | } 342 | ``` 343 | 344 | 在这里会重新封装endpoints然后调用EDSUpdate进行更新。 345 | 346 | ```go 347 | func (s *DiscoveryServer) EDSUpdate(clusterID, serviceName string, namespace string, 348 | istioEndpoints []*model.IstioEndpoint) error { 349 | inboundEDSUpdates.Increment() 350 | s.edsUpdate(clusterID, serviceName, namespace, istioEndpoints, false) 351 | return nil 352 | } 353 | 354 | func (s *DiscoveryServer) edsUpdate(clusterID, serviceName string, namespace string, 355 | istioEndpoints []*model.IstioEndpoint, internal bool) { 356 | s.mutex.Lock() 357 | defer s.mutex.Unlock() 358 | requireFull := false 359 | 360 | ... 361 | //找到之前缓存的服务 362 | if _, f := s.EndpointShardsByService[serviceName]; !f { 363 | s.EndpointShardsByService[serviceName] = map[string]*EndpointShards{} 364 | } 365 | ep, f := s.EndpointShardsByService[serviceName][namespace] 366 | //不存在则初始化 367 | if !f { 368 | ep = &EndpointShards{ 369 | Shards: map[string][]*model.IstioEndpoint{}, 370 | ServiceAccounts: map[string]bool{}, 371 | } 372 | s.EndpointShardsByService[serviceName][namespace] = ep 373 | if !internal { 374 | adsLog.Infof("Full push, new service %s", serviceName) 375 | requireFull = true 376 | } 377 | } 378 | ... 379 | ep.mutex.Lock() 380 | ep.Shards[clusterID] = istioEndpoints 381 | ep.ServiceAccounts = serviceAccounts 382 | ep.mutex.Unlock() 383 | 384 | if !internal { 385 | var edsUpdates map[string]struct{} 386 | if !requireFull { 387 | edsUpdates = map[string]struct{}{serviceName: {}} 388 | } 389 | //配置更新 390 | s.ConfigUpdate(&model.PushRequest{ 391 | Full: requireFull, 392 | NamespacesUpdated: map[string]struct{}{namespace: {}}, 393 | ConfigTypesUpdated: map[resource.GroupVersionKind]struct{}{collections.IstioNetworkingV1Alpha3Serviceentries.Resource().GroupVersionKind(): {}}, 394 | EdsUpdates: edsUpdates, 395 | Reason: []model.TriggerReason{model.EndpointUpdate}, 396 | }) 397 | } 398 | } 399 | ``` 400 | 401 | edsUpdate方法里面实际上就是做了两件事,一是更新缓存,二是调用ConfigUpdate进行配置更新。 402 | 403 | ConfigUpdate资源更新实际上就是通过事件分发执行xDS分发,这块的细节我们稍后再讲。 404 | 405 | ## 总结 406 | 407 | 通过这篇我们掌握了服务发现是通过k8s的Informer来注册监听Service、EndPoint、nodes、pods等资源的更新事件,然后通过事件驱动模型执行回调函数,再调用xDS的ConfigUpdate来执行异步更新配置的操作。 408 | 409 | ## Reference 410 | 411 | https://www.servicemesher.com/blog/istio-analysis-4/ 412 | 413 | https://www.cnblogs.com/163yun/p/8962278.html 414 | 415 | https://www.servicemesher.com/blog/envoy-proxy-config-deep-dive/ -------------------------------------------------------------------------------- /深入istio/2.深入Istio:Pilot服务发现/istio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/2.深入Istio:Pilot服务发现/istio.png -------------------------------------------------------------------------------- /深入istio/3.深入Istio.md: -------------------------------------------------------------------------------- 1 | # 3.深入Istio源码:Pilot配置规则ConfigController 2 | 3 | Config Controller用于管理各种配置数据,包括用户创建的流量管理规则和策略。Istio目前支持三种类型的Config Controller: 4 | 5 | * MCP:是一种网络配置协议,用于隔离Pilot和底层平台(文件系统、K8s),使得Pilot无须感知底层平台的差异,从而达到解耦的目的。 6 | * File:通过监视器周期性地读取本地配置文件,将配置规则缓存在内存中,并维护配置的增加、更新、删除事件,当缓存由变化的时候,异步通知执行事件回调。 7 | * Kubernetes:基于k8s的Config发现利用了k8s Informer的监听能力。在k8s集群中,Config以CustomResource的形式存在。通过监听apiserver配置规则资源,维护所有资源的缓存Store,并触发事件处理回调函数。 8 | 9 | ## ConfigController初始化 10 | 11 | ConfigController是在initConfigController中被初始化的,在initConfigController方法中会调用makeKubeConfigController进行controller的初始化。 12 | 13 | ```go 14 | func (s *Server) makeKubeConfigController(args *PilotArgs) (model.ConfigStoreCache, error) { 15 | //创建configClient 16 | configClient, err := controller.NewClient(args.Config.KubeConfig, "", collections.Pilot, 17 | args.Config.ControllerOptions.DomainSuffix, buildLedger(args.Config), args.Revision) 18 | if err != nil { 19 | return nil, multierror.Prefix(err, "failed to open a config client.") 20 | } 21 | //创建controller,并为config资源设置监听 22 | return controller.NewController(configClient, args.Config.ControllerOptions), nil 23 | } 24 | 25 | func NewController(client *Client, options controller2.Options) model.ConfigStoreCache { 26 | log.Infof("CRD controller watching namespaces %q", options.WatchedNamespace) 27 | 28 | // The queue requires a time duration for a retry delay after a handler error 29 | out := &controller{ 30 | client: client, 31 | queue: queue.NewQueue(1 * time.Second), 32 | kinds: make(map[resource.GroupVersionKind]*cacheHandler), 33 | } 34 | 35 | // add stores for CRD kinds 36 | //获取所有的CRD类型 37 | for _, s := range client.Schemas().All() { 38 | //为每一种Config资源都创建一个informer,监听所有的Config资源 39 | out.addInformer(s, options.WatchedNamespace, options.ResyncPeriod) 40 | } 41 | return out 42 | } 43 | ``` 44 | 45 | 初始化完controller之后会获取所有的CRD类型,为每一种Config资源都创建一个informer,监听所有的Config资源。 46 | 47 | ```go 48 | Pilot = collection.NewSchemasBuilder(). 49 | //MeshPolicy 50 | MustAdd(IstioAuthenticationV1Alpha1Meshpolicies). 51 | MustAdd(IstioAuthenticationV1Alpha1Policies). 52 | MustAdd(IstioConfigV1Alpha2Httpapispecbindings). 53 | MustAdd(IstioConfigV1Alpha2Httpapispecs). 54 | MustAdd(IstioMixerV1ConfigClientQuotaspecbindings). 55 | MustAdd(IstioMixerV1ConfigClientQuotaspecs). 56 | //DestinationRule 57 | MustAdd(IstioNetworkingV1Alpha3Destinationrules). 58 | //EnvoyFilter 59 | MustAdd(IstioNetworkingV1Alpha3Envoyfilters). 60 | //Gateway 61 | MustAdd(IstioNetworkingV1Alpha3Gateways). 62 | //ServiceEntry 63 | MustAdd(IstioNetworkingV1Alpha3Serviceentries). 64 | //Sidecar 65 | MustAdd(IstioNetworkingV1Alpha3Sidecars). 66 | //VirtualService 67 | MustAdd(IstioNetworkingV1Alpha3Virtualservices). 68 | MustAdd(IstioRbacV1Alpha1Clusterrbacconfigs). 69 | MustAdd(IstioRbacV1Alpha1Rbacconfigs). 70 | MustAdd(IstioRbacV1Alpha1Servicerolebindings). 71 | MustAdd(IstioRbacV1Alpha1Serviceroles). 72 | MustAdd(IstioSecurityV1Beta1Authorizationpolicies). 73 | MustAdd(IstioSecurityV1Beta1Peerauthentications). 74 | MustAdd(IstioSecurityV1Beta1Requestauthentications). 75 | Build() 76 | ``` 77 | 78 | 这里定义好了所有要用到的Config资源类型,主要涉及网络配置、认证、鉴权、策略管理等。 79 | 80 | ## ConfigController事件处理 81 | 82 | 下面我们看一下controller定义: 83 | 84 | ```go 85 | type controller struct { 86 | client *Client 87 | queue queue.Instance 88 | kinds map[resource.GroupVersionKind]*cacheHandler 89 | } 90 | ``` 91 | 92 | client是调用`controller.NewClient`初始化的client;queue会在Informer监听到资源的变动的时候将数据push到队列中,controller在调用run方法的时候单独运行一个线程运行queue中的函数;kinds在调用addInformer方法的时候初始化进去。 93 | 94 | queue.Instance的定义如下: 95 | 96 | ```go 97 | type Task func() error 98 | 99 | type Instance interface { 100 | Push(task Task) 101 | Run(<-chan struct{}) 102 | } 103 | 104 | type queueImpl struct { 105 | delay time.Duration 106 | tasks []Task 107 | cond *sync.Cond 108 | closing bool 109 | } 110 | ``` 111 | 112 | queueImpl继承了Instance接口,在调用push方法的时候,会将Task放入到tasks数组中,并在调用Run方法的时候消费数组中的数据。 113 | 114 | controller继承了ConfigStoreCache接口: 115 | 116 | ```go 117 | type ConfigStoreCache interface { 118 | ConfigStore 119 | // 注册规则事件处理函数 120 | RegisterEventHandler(kind resource.GroupVersionKind, handler func(Config, Config, Event)) 121 | // 运行 122 | Run(stop <-chan struct{}) 123 | 124 | // 配置缓存是否已同步 125 | HasSynced() bool 126 | } 127 | ``` 128 | 129 | ConfigStoreCache通过RegisterEventHandler接口为上面提到的配置资源都注册事件处理函数,通过Run方法启动控制器。 130 | 131 | ```go 132 | func (c *controller) Run(stop <-chan struct{}) { 133 | log.Infoa("Starting Pilot K8S CRD controller") 134 | go func() { 135 | cache.WaitForCacheSync(stop, c.HasSynced) 136 | //单独启动一个线程运行queue里面的函数 137 | c.queue.Run(stop) 138 | }() 139 | 140 | for _, ctl := range c.kinds { 141 | go ctl.informer.Run(stop) 142 | } 143 | 144 | <-stop 145 | log.Info("controller terminated") 146 | } 147 | ``` 148 | 149 | 在调用Run方法的时候会单独的启动一个线程调用queue的Run方法消费队列中的数据,并遍历所有的配置信息,调用informer的Run方法开启监听。 150 | 151 | 监听器的EventHandler通过如下代码注册: 152 | 153 | ```go 154 | func (c *controller) newCacheHandler( 155 | schema collection.Schema, 156 | o runtime.Object, 157 | otype string, 158 | resyncPeriod time.Duration, 159 | lf cache.ListFunc, 160 | wf cache.WatchFunc) *cacheHandler { 161 | informer := cache.NewSharedIndexInformer( 162 | &cache.ListWatch{ListFunc: lf, WatchFunc: wf}, o, 163 | resyncPeriod, cache.Indexers{}) 164 | 165 | h := &cacheHandler{ 166 | c: c, 167 | schema: schema, 168 | informer: informer, 169 | } 170 | 171 | informer.AddEventHandler( 172 | cache.ResourceEventHandlerFuncs{ 173 | AddFunc: func(obj interface{}) { 174 | incrementEvent(otype, "add") 175 | //将ADD事件发送至队列 176 | c.queue.Push(func() error { 177 | return h.onEvent(nil, obj, model.EventAdd) 178 | }) 179 | }, 180 | UpdateFunc: func(old, cur interface{}) { 181 | if !reflect.DeepEqual(old, cur) { 182 | incrementEvent(otype, "update") 183 | //将Update事件发送至队列 184 | c.queue.Push(func() error { 185 | return h.onEvent(old, cur, model.EventUpdate) 186 | }) 187 | } else { 188 | incrementEvent(otype, "updatesame") 189 | } 190 | }, 191 | DeleteFunc: func(obj interface{}) { 192 | incrementEvent(otype, "delete") 193 | //将Delete事件发送至队列 194 | c.queue.Push(func() error { 195 | return h.onEvent(nil, obj, model.EventDelete) 196 | }) 197 | }, 198 | }) 199 | 200 | return h 201 | } 202 | ``` 203 | 204 | 当Config资源创建、更新、删除时,EventHandler创建任务对象并将其发送到任务队列中,然后由任务处理线程处理。当对应的事件被调用的时候会触发onEvent方法,会调用到cacheHandler的onEvent方法,最后设置完毕后将cacheHandler返回,controller会将此cacheHandler设置到kinds数组中存下来。 205 | 206 | 下面我们看一下cacheHandler的定义: 207 | 208 | ```go 209 | type cacheHandler struct { 210 | c *controller 211 | schema collection.Schema 212 | informer cache.SharedIndexInformer 213 | handlers []func(model.Config, model.Config, model.Event) 214 | } 215 | ``` 216 | 217 | cacheHandler在上面初始化的时候,会传入对应的controller、Schema、informer,然后在调用configController的RegisterEventHandler方法的时候会初始化对应的configHandler。 218 | 219 | configController的RegisterEventHandler方法会在初始化DiscoveryService的时候调用initEventHandlers方法进行初始化: 220 | 221 | ```go 222 | func (s *Server) initEventHandlers() error { 223 | ... 224 | if s.configController != nil { 225 | configHandler := func(old, curr model.Config, _ model.Event) { 226 | pushReq := &model.PushRequest{ 227 | Full: true, 228 | ConfigTypesUpdated: map[resource.GroupVersionKind]struct{}{curr.GroupVersionKind(): {}}, 229 | Reason: []model.TriggerReason{model.ConfigUpdate}, 230 | } 231 | s.EnvoyXdsServer.ConfigUpdate(pushReq) 232 | } 233 | //遍历所有的资源 234 | for _, schema := range collections.Pilot.All() { 235 | // This resource type was handled in external/servicediscovery.go, no need to rehandle here. 236 | //ServiceEntry 这个资源不在这里注册,感兴趣的朋友可以自己找一下 237 | if schema.Resource().GroupVersionKind() == collections.IstioNetworkingV1Alpha3Serviceentries. 238 | Resource().GroupVersionKind() { 239 | continue 240 | } 241 | //注册configHandler到configController中 242 | s.configController.RegisterEventHandler(schema.Resource().GroupVersionKind(), configHandler) 243 | } 244 | } 245 | 246 | return nil 247 | } 248 | ``` 249 | 250 | initEventHandlers会调用collections.Pilot.All方法获取所有的资源配置,然后遍历调用RegisterEventHandler方法将configHandler函数注册到cacheHandler的handlers中,至于configHandler函数做了什么,我们到下一篇讲XdsServer的时候再讲。 251 | 252 | 这一部分的代码是比较绕的,这里画个图理解一下吧。 253 | 254 | ![Group2](3.深入Istio/Group2.png) 255 | 256 | 257 | 258 | 整个执行流程为: 259 | 260 | ![configserver (1)](3.深入Istio/configserver (1).png) 261 | 262 | 263 | 264 | ## 总结 265 | 266 | 至此,ConfigController的核心原理及工作流程就介绍完毕了。本篇主要讲解了我们常用的Istio的Gateway、DestinationRule及VirtualService等配置是如何被Istio监听到并作出相应改变的。希望大家能有所收获。 267 | 268 | ## Reference 269 | 270 | https://ruofeng.me/2018/11/08/how-does-istio-pilot-push-eds-config/ 271 | 272 | https://zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysi 273 | 274 | https://www.servicemesher.com/blog/envoy-proxy-config-deep-dive/ 275 | 276 | https://www.cnblogs.com/163yun/p/8962278.html -------------------------------------------------------------------------------- /深入istio/3.深入Istio/Group2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/3.深入Istio/Group2.png -------------------------------------------------------------------------------- /深入istio/3.深入Istio/Group3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/3.深入Istio/Group3.png -------------------------------------------------------------------------------- /深入istio/3.深入Istio/configserver (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/3.深入Istio/configserver (1).png -------------------------------------------------------------------------------- /深入istio/3.深入Istio/configserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/3.深入Istio/configserver.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/Blank diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/Blank diagram.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/Group 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/Group 3.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/Group 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/Group 4.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/Group 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/Group 5.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/Group 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/Group 6.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/dsServerStart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/dsServerStart.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/image-20201130231001341.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/image-20201130231001341.png -------------------------------------------------------------------------------- /深入istio/4.深入Istio/startPush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/4.深入Istio/startPush.png -------------------------------------------------------------------------------- /深入istio/5.深入Istio源码:Pilot-agent/Group 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/5.深入Istio源码:Pilot-agent/Group 7.png -------------------------------------------------------------------------------- /深入istio/5.深入Istio源码:Pilot-agent/Group 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/5.深入Istio源码:Pilot-agent/Group 8.png -------------------------------------------------------------------------------- /深入istio/从一个例子入手Istio.md: -------------------------------------------------------------------------------- 1 | # 从一个例子入手Istio 2 | 3 | ![74617380_p0_master1200](从一个例子入手Istio/74617380_p0_master1200.jpg) 4 | 5 | > 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 6 | > 7 | > 本文使用的Istio源码是 release 1.5。 8 | > 9 | > 本篇是Istio系列的第一篇,希望大家接下来能和我一起学习进步。 10 | 11 | 封面图是[Klegs](https://www.pixiv.net/users/20345732)的作品,颜色有一种深邃感,我很喜欢。这篇文章是Istio系列文章的开篇,主要从一个例子入手讲一下Istio,并讲解一些基础概念,后面会基于这个例子来展开讲解istio里面的实现原理。 12 | 13 | Istio里面有很多有趣的内容,希望大家能一起来学习,感受Istio的魅力,当然Istio是和k8s是分不开的,所以也需要掌握了一定k8s知识能力才能进行学习,还没有掌握的同学不妨看看我的系列文章来进行学习:[深入k8s系列文章](https://www.luozhiyun.com/archives/tag/%e6%b7%b1%e5%85%a5k8s)。 14 | 15 | 16 | 17 | ## 基本概念 18 | 19 | 先来说一下什么是Service Mesh(服务网格),一般来说Service Mesh是一种控制处理服务间通信的基础设施层,可以在云原生场景下帮助应用程序在复杂的服务拓扑间可靠的传递请求。在实际使用中,服务网格一般是通过一组轻量网络代理来执行治理逻辑的,并且网络代理和应用绑定在一起,但是对应用来说是无感的。 20 | 21 | 下面用一张经典的网络示意图来表示一下Service Mesh: 22 | 23 | ![image-20201024215839257](从一个例子入手Istio/image-20201024215839257.png) 24 | 25 | 那么Istio又是什么呢?Istio就是一个Service Mesh实现的形态,用于服务治理的开放平台,并且Istio是与K8s紧密结合的适用于云原生场景的平台。 26 | 27 | 下面我们看看Istio的架构图: 28 | 29 | ![The overall architecture of an Istio-based application.](从一个例子入手Istio/arch.svg) 30 | 31 | Istio分别由数据平面(Data plane)和控制平面(Control plane)组成。 32 | 33 | 数据平面由网格内的Proxy代理和应用组成,这些代理以sidecar的形式和应用服务一起部署。每一个 sidecar会接管进入和离开服务的流量,并配合控制平面完成流量控制等方面的功能。 34 | 35 | 控制平面用于控制和管理数据平面中的sidecar代理,完成配置的分发、服务发现、和授权鉴权等功能,可以统一的对数据平面进行管理。 36 | 37 | 在上面的组件中,Proxy代理默认使用Envoy作为sidecar代理,Envoy是由Lyft内部于2016年开发的,其性能和资源占用都有着很好的表现,能够满足服务网格中对透明代理的轻量高性能的要求。 38 | 39 | Pilot组件主要功能是将路由规则等配置信息转换为sidecar可以识别的信息,并下发给数据平面,完成流量控制相关的功能。 40 | 41 | Citadel是专门负责安全的组件,内置有身份和证书管理功能,可以实现较为强大的授权和认证等操作。 42 | 43 | Galley主要负责配置的验证、提取和处理等功能。 44 | 45 | # 安装 Istio 46 | 47 | 本地需要准备一台机器上面安装有K8s,可以使用我在讲k8s的时候部署的机器:[1.深入k8s:k8s部署&在k8s中运行第一个程序](https://www.luozhiyun.com/archives/314)。 48 | 49 | 因为Istio的发展太过于迅速了,我这里是使用1.5.10的版本进行举例,大家可以去这里下载好应用包:https://github.com/istio/istio/releases/tag/1.5.10。 50 | 51 | 解压好之后里面会包含如下文件目录: 52 | 53 | | 目录 | 包含内容 | 54 | | --------- | ------------------------------------------------------------ | 55 | | `bin` | 包含 istioctl 的客户端文件 | 56 | | `install` | 包含 Consul、GCP 和 Kubernetes 平台的 [Istio](https://www.servicemesher.com/istio-handbook/GLOSSARY.html#istio) 安装脚本和文件 | 57 | | `samples` | 包含示例应用程序 | 58 | | `tools` | 包含用于性能测试和在本地机器上进行测试的脚本 | 59 | 60 | 然后我们将istioctl客户端路径加入环境变量中: 61 | 62 | ```sh 63 | [root@localhost ~]# export PATH=$PATH:$(pwd)/istio-1.5.10/bin 64 | ``` 65 | 66 | istio不同的版本会有不同的差异,如下表格: 67 | 68 | | | default | demo | minimal | remote | 69 | | ---------------- | -------- | ---------- | -------- | -------------- | 70 | | 使用场景 | 生产环境 | 展示、学习 | 基本流控 | 多网格共享平面 | 71 | | **核心组件** | | | | | 72 | | - pilot | Y | Y | Y | | 73 | | - ingressgateway | Y | Y | | | 74 | | - engressgateway | | Y | | | 75 | 76 | 我们这里用于学习使用,所以使用demo进行安装: 77 | 78 | ```sh 79 | [root@localhost ~]# istioctl manifest apply --set profile=demo 80 | ``` 81 | 82 | 运行完命令后显示:Installation compelte代表安装完成。 83 | 84 | 安装好之后会安装一个新的namespace:istio-system 85 | 86 | 我们可以指定ns来获取它下面的pod: 87 | 88 | ```sh 89 | [root@localhost ~]# kubectl get pod -n istio-system 90 | NAME READY STATUS RESTARTS AGE 91 | grafana-764dbb499-pxs84 1/1 Running 0 17h 92 | istio-egressgateway-775f9cd579-lsw5q 1/1 Running 0 17h 93 | istio-ingressgateway-5d75d8897-dn8vz 1/1 Running 0 17h 94 | istio-tracing-9dd6c4f7c-pwq22 1/1 Running 0 17h 95 | istiod-749c4cf7f8-xgnv8 1/1 Running 0 17h 96 | kiali-869c6894c5-l72sc 1/1 Running 0 17h 97 | prometheus-79757ffc4-qxccg 2/2 Running 0 17h 98 | ``` 99 | 100 | # Bookinfo 示例 101 | 102 | Bookinfo 是 Istio 社区官方推荐的示例应用之一。它可以用来演示多种Istio的特性,并且它是一个异构的微服务应用。应用由四个单独的微服务构成:productpage、details、reviews、ratings。 103 | 104 | - `productpage` 会调用 `details` 和 `reviews` 两个微服务,用来生成页面由python来编写。 105 | - `details` 中包含了书籍的信息由,Ruby来编写 106 | - `reviews` 中包含了书籍相关的评论。它还会调用 `ratings` 微服务,由java编写。 107 | - `ratings` 中包含了由书籍评价组成的评级信息,由Node js编写。 108 | 109 | 下面这个图展示了调用关系: 110 | 111 | ![image-20201025163623769](从一个例子入手Istio/image-20201025163623769.png) 112 | 113 | 如果我们的应用要接入Istio服务,那么就需要在这些应用里面都打上sidecar,使服务所有的出入流量都被sidecar所劫持,然后就可以利用istio为应用提供服务路由、遥测数据收集以及策略实施等功能。 114 | 115 | ### 启动服务 116 | 117 | 要实现注入sidecar有两种方式,一个是手动注入,一个是自动注入。 118 | 119 | 手动注入可以通过使用:`istioctl kube-inject -f xxx.yaml | kubectl apply -f - `来实现。 120 | 121 | 自动注入的需要为应用部署的命令空间打上标签 `istio-injection=enabled`,如果在default空间部署应用,那么可以这么做: 122 | 123 | ```sh 124 | [root@localhost ~]# kubectl label namespace default istio-injection=enabled 125 | ``` 126 | 127 | 这里istio会利用k8s的webhook机制为每个创建的pod都自动注入sidecar,具体是如何做的,下一篇我们再讲。 128 | 129 | 然后我们使用istio中自带的例子部署应用: 130 | 131 | ```sh 132 | [root@localhost ~]# kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml 133 | ``` 134 | 135 | 过一段时间后确认应用都已启动和部署成功: 136 | 137 | ```sh 138 | [root@localhost ~]# kubectl get pod 139 | NAME READY STATUS RESTARTS AGE 140 | details-v1-6c9f8bcbcb-shltm 2/2 Running 0 17h 141 | productpage-v1-7df7cb7f86-h75dd 2/2 Running 0 17h 142 | ratings-v1-65cff55fb8-9vh2x 2/2 Running 0 17h 143 | reviews-v1-7bccdbbf96-m2xbf 2/2 Running 0 17h 144 | reviews-v2-7c9685df46-lljzt 2/2 Running 0 17h 145 | reviews-v3-58fc46b64-294f4 2/2 Running 0 17h 146 | sleep-8f795f47d-6pw96 2/2 Running 0 16h 147 | ``` 148 | 149 | 我们可以用describe命令查看其中的pod: 150 | 151 | ```sh 152 | [root@localhost ~]# kubectl describe pod details-v1-6c9f8bcbcb-shltm 153 | ... 154 | Init Containers: 155 | istio-init: 156 | Container ID: docker://6d14ccc83bd119236bf8fda13f6799609c87891be9b2c5af7cbf7d8c913ce17e 157 | Image: docker.io/istio/proxyv2:1.5.10 158 | Image ID: docker-pullable://istio/proxyv2@sha256:abbe8ad6d50474814f1aa9316dafc2401fbba89175638446f01afc36b5a37919 159 | Port: 160 | Host Port: 161 | Command: 162 | istio-iptables 163 | -p 164 | 15001 165 | -z 166 | 15006 167 | -u 168 | 1337 169 | -m 170 | REDIRECT 171 | -i 172 | * 173 | -x 174 | 175 | -b 176 | * 177 | -d 178 | 15090,15020 179 | ... 180 | Containers: 181 | details: 182 | Container ID: docker://ed216429216ea1b8a1ba20960590edb7322557467c38cceff3c3e847bcff0a14 183 | Image: docker.io/istio/examples-bookinfo-details-v1:1.15.1 184 | Image ID: docker-pullable://istio/examples-bookinfo-details-v1@sha256:344b1c18703ab1e51aa6d698f459c95ea734f8317d779189f4638de7a00e61ae 185 | ... 186 | istio-proxy: 187 | Container ID: docker://a3862cc8f53198c8f86a911089e73e00f4cc4aa02eea05aaeb0bd267a8e98482 188 | Image: docker.io/istio/proxyv2:1.5.10 189 | Image ID: docker-pullable://istio/proxyv2@sha256:abbe8ad6d50474814f1aa9316dafc2401fbba89175638446f01afc36b5a37919 190 | Port: 15090/TCP 191 | Host Port: 0/TCP 192 | Args: 193 | ... 194 | ``` 195 | 196 | 可以看到里面有一个初始化的Init Containers,用于设置 `iptables` 规则。还注入了istio-proxy,这个容器是真正的 Sidecar。 197 | 198 | 为了能让应用程序可以从外部访问 k8s 集群,需要安装gateway: 199 | 200 | ```sh 201 | [root@localhost ~]# kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml 202 | ``` 203 | 204 | 检查gateway: 205 | 206 | ```sh 207 | [root@localhost ~]# kubectl get gateway 208 | NAME AGE 209 | bookinfo-gateway 17h 210 | ``` 211 | 212 | 因为我是单机环境,未使用外部负载均衡器,需要通过 node port 访问,然后我们查看node port: 213 | 214 | ```sh 215 | [root@localhost ~]# kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}' 216 | 22412 217 | ``` 218 | 219 | 我的k8snode地址是192.168.13.129,然后我们在浏览器输入:http://192.168.13.129:22412/productpage就可以访到对应的页面了,多刷新几次,会发现访问到不同的Book Reviews,因为默认使用的轮询策略。 220 | 221 | ## 总结 222 | 223 | 这一篇讲了一下如何安装istio,以及如何部署应用,使用istio来完成一个实例,比较简单和基础,里面的很多细节我这里都一笔带过了,但是在后面的一些内容会基于这个例子来进行讲解里面的具体实现原理,所以这篇文章还是有些必要的。 224 | 225 | ## Reference 226 | 227 | [What's a service mesh?](https://www.redhat.com/en/topics/microservices/what-is-a-service-mesh) 228 | 229 | [What is Istio?](https://istio.io/latest/docs/concepts/what-is-istio/) 230 | 231 | https://istio.io/latest/docs/examples/bookinfo/ -------------------------------------------------------------------------------- /深入istio/从一个例子入手Istio/74617380_p0_master1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/从一个例子入手Istio/74617380_p0_master1200.jpg -------------------------------------------------------------------------------- /深入istio/从一个例子入手Istio/arch.svg: -------------------------------------------------------------------------------- 1 | archIstio MeshIngresstrafficEgresstrafficService AProxyService BProxyDiscoveryConfigurationCertificatesMesh trafficControl planeDataplaneistiodGalleyPilotCitadel -------------------------------------------------------------------------------- /深入istio/从一个例子入手Istio/image-20201024215839257.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/从一个例子入手Istio/image-20201024215839257.png -------------------------------------------------------------------------------- /深入istio/从一个例子入手Istio/image-20201025163623769.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/从一个例子入手Istio/image-20201025163623769.png -------------------------------------------------------------------------------- /深入istio/深入Istio/Group 1-1606017533527.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/深入Istio/Group 1-1606017533527.png -------------------------------------------------------------------------------- /深入istio/深入Istio/Group 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/深入Istio/Group 1.png -------------------------------------------------------------------------------- /深入istio/深入Istio/Group 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/深入Istio/Group 2.png -------------------------------------------------------------------------------- /深入istio/深入Istio/serviceController.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/深入Istio/serviceController.png -------------------------------------------------------------------------------- /深入istio/深入Istio/serviceController流程图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入istio/深入Istio/serviceController流程图.png -------------------------------------------------------------------------------- /深入k8s/1.深入k8s:在k8s中运行第一个程序.md: -------------------------------------------------------------------------------- 1 | # 1.深入k8s:在k8s中运行第一个程序 2 | 3 | ![014f5a5f1568aca801215aa0a9da5d.jpg@3000w_1l_0o_100sh](https://img.luozhiyun.com/20200720231934.jpg) 4 | 5 | ## 搭建k8s单点实验环境 6 | 7 | 由于在国内网络问题,我们无法很好的使用minikube进行部署k8s实验环境,所以可以使用阿里提供的[minikube](https://github.com/AliyunContainerService/minikube)进行搭建。除了minikube,也可以使用[kubeasz](https://github.com/easzlab/kubeasz)进行部署。 8 | 9 | 下面我基于kubeaze给出部署方法,部署方法都参照https://github.com/easzlab/kubeasz,其中附上我的踩坑经验。 10 | 11 | ### 1.基础系统配置 12 | 13 | - 准备一台虚机配置内存2G/硬盘30G以上 14 | - 最小化安装`Ubuntu 16.04 server`或者`CentOS 7 Minimal` 15 | - 配置基础网络、更新源、SSH登录等 16 | 17 | **注意:** 确保在干净的系统上开始安装,不能使用曾经装过kubeadm或其他k8s发行版的环境 18 | 19 | ### 2.下载文件 20 | 21 | - 下载工具脚本easzup,举例使用kubeasz版本2.2.1 22 | 23 | ``` bash 24 | export release=2.2.1 25 | curl -C- -fLO --retry 3 https://github.com/easzlab/kubeasz/releases/download/${release}/easzup 26 | chmod +x ./easzup 27 | ``` 28 | 29 | - 使用工具脚本下载 30 | 31 | 默认下载最新推荐k8s/docker等版本,使用命令`./easzup` 查看工具脚本的帮助信息 32 | 33 | 设置docker源: 34 | 35 | ```json 36 | { 37 | "registry-mirrors": [ 38 | "https://eig40bb2.mirror.aliyuncs.com", 39 | "https://docker.mirrors.ustc.edu.cn", 40 | "http://hub-mirror.c.163.com", 41 | "https://reg-mirror.qiniu.com" 42 | ], 43 | "max-concurrent-downloads": 10, 44 | "log-driver": "json-file", 45 | "log-level": "warn", 46 | "log-opts": { 47 | "max-size": "10m", 48 | "max-file": "3" 49 | }, 50 | "data-root": "/var/lib/docker" 51 | } 52 | ``` 53 | 54 | 添加完之后刷新一下配置: 55 | 56 | ``` 57 | systemctl daemon-reload 58 | systemctl restart docker 59 | ``` 60 | 61 | ``` bash 62 | # 举例使用 k8s 版本 v1.18.2,docker 19.03.5 63 | ./easzup -D -d 19.03.5 -k v1.18.2 64 | ``` 65 | 66 | - 可选下载离线系统包 (适用于无法使用yum/apt仓库情形) 67 | 68 | ``` bash 69 | ./easzup -P 70 | ``` 71 | 72 | 上述脚本运行成功后,所有文件(kubeasz代码、二进制、离线镜像)均已整理好放入目录`/etc/ansible` 73 | 74 | - `/etc/ansible` 包含 kubeasz 版本为 ${release} 的发布代码 75 | - `/etc/ansible/bin` 包含 k8s/etcd/docker/cni 等二进制文件 76 | - `/etc/ansible/down` 包含集群安装时需要的离线容器镜像 77 | - `/etc/ansible/down/packages` 包含集群安装时需要的系统基础软件 78 | 79 | ### 3.安装集群 80 | 81 | - 容器化运行 kubeasz,详见[文档](docker_kubeasz.md) 82 | 83 | ``` 84 | ./easzup -S 85 | ``` 86 | 87 | - 使用默认配置安装 aio 集群 88 | 89 | ``` 90 | docker exec -it kubeasz easzctl start-aio 91 | ``` 92 | 93 | ### 4.验证安装 94 | 95 | 如果提示kubectl: command not found,退出重新ssh登录一下,环境变量生效即可 96 | 97 | ``` bash 98 | $ kubectl version # 验证集群版本 99 | $ kubectl get node # 验证节点就绪 (Ready) 状态 100 | $ kubectl get pod -A # 验证集群pod状态,默认已安装网络插件、coredns、metrics-server等 101 | $ kubectl get svc -A # 验证集群服务状态 102 | ``` 103 | 104 | ## 搭建k8s集群环境 105 | 106 | 我们打算搭建一个这样的三节点的集群: 107 | 108 | ![k8s集群图](在k8s中运行第一个程序/k8s集群图.png) 109 | 110 | ### 1. 准备服务器 111 | 112 | 首先3台虚机,搭建一个多主高可用集群。 113 | 114 | - 推荐内存2G/硬盘30G以上 115 | - 最小化安装`Ubuntu 16.04 server`或者`CentOS 7 Minimal` 116 | - 配置基础网络、更新源、SSH登录等 117 | 118 | ### 2. 在每个节点安装ansible依赖工具 119 | 120 | CentOS 7 请执行以下脚本: 121 | 122 | ``` 123 | # 文档中脚本默认均以root用户执行 124 | yum update 125 | # 安装python 126 | yum install python -y 127 | ``` 128 | 129 | 安装pip的时候先安装 130 | 131 | ``` 132 | yum -y install epel-release 133 | ``` 134 | 135 | 安装pip 136 | 137 | ``` 138 | yum install git python-pip -y 139 | ``` 140 | 141 | pip 安装 ansible 142 | 143 | ``` 144 | # CentOS 7 145 | yum install git python-pip -y 146 | # pip安装ansible(国内如果安装太慢可以直接用pip阿里云加速) 147 | pip install pip --upgrade -i https://mirrors.aliyun.com/pypi/simple/ 148 | pip install ansible==2.6.18 netaddr==0.7.19 -i https://mirrors.aliyun.com/pypi/simple/ 149 | ``` 150 | 151 | 在ansible控制端配置免密码登录 152 | 153 | ``` 154 | # 更安全 Ed25519 算法 155 | ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519 156 | # 或者传统 RSA 算法 157 | ssh-keygen -t rsa -b 2048 -N '' -f ~/.ssh/id_rsa 158 | 159 | ssh-copy-id 192.168.13.131 160 | ssh-copy-id 192.168.13.132 161 | ssh-copy-id 192.168.13.133 162 | ``` 163 | 164 | 如果这一步不设置,那么在部署的时候无法访问到相应的节点会报如下错: 165 | 166 | ``` 167 | 192.168.13.130 | UNREACHABLE! => { 168 | "changed": false, 169 | "msg": "SSH Error: data could not be sent to remote host \"192.168.13.130\". Make sure this host can be reached over ssh", 170 | "unreachable": true 171 | } 172 | ``` 173 | 174 | ### 3.在ansible控制端编排k8s安装 175 | 176 | - 4.0 下载项目源码 177 | - 4.1 下载二进制文件 178 | - 4.2 下载离线docker镜像 179 | 180 | 推荐使用 easzup 脚本下载 4.0/4.1/4.2 所需文件;运行成功后,所有文件(kubeasz代码、二进制、离线镜像)均已整理好放入目录`/etc/ansible` 181 | 182 | ``` 183 | # 下载工具脚本easzup,举例使用kubeasz版本2.0.2 184 | export release=2.0.2 185 | curl -C- -fLO --retry 3 https://github.com/easzlab/kubeasz/releases/download/${release}/easzup 186 | chmod +x ./easzup 187 | # 使用工具脚本下载 188 | ./easzup -D 189 | ``` 190 | 191 | ### 4.配置集群参数 192 | 193 | * 必要配置:`cd /etc/ansible && cp example/hosts.multi-node hosts`, 然后实际情况修改此hosts文件 194 | 195 | 根据我们上图的介绍,我们将hosts改成如下: 196 | 197 | ``` 198 | # 'etcd' cluster should have odd member(s) (1,3,5,...) 199 | # variable 'NODE_NAME' is the distinct name of a member in 'etcd' cluster 200 | [etcd] 201 | 192.168.13.131 NODE_NAME=etcd1 202 | 192.168.13.132 NODE_NAME=etcd2 203 | 192.168.13.133 NODE_NAME=etcd3 204 | 205 | # master node(s) 206 | [kube-master] 207 | 192.168.13.131 208 | 209 | # work node(s) 210 | [kube-node] 211 | 192.168.13.131 212 | 192.168.13.132 213 | 192.168.13.133 214 | 215 | ... 216 | ``` 217 | 218 | 只改如上配置,其他的可不动。 219 | 220 | * 验证ansible 安装:`ansible all -m ping` 正常能看到节点返回 SUCCESS 221 | 222 | * 开始安装 如果你对集群安装流程不熟悉,请阅读项目首页 **安装步骤** 讲解后分步安装,并对 **每步都进行验证** 223 | 224 | ``` 225 | # 分步安装 226 | ansible-playbook 01.prepare.yml 227 | ansible-playbook 02.etcd.yml 228 | ansible-playbook 03.docker.yml 229 | ansible-playbook 04.kube-master.yml 230 | ansible-playbook 05.kube-node.yml 231 | ansible-playbook 06.network.yml 232 | ansible-playbook 07.cluster-addon.yml 233 | ``` 234 | 235 | * 如果不想分步安装,那么可以一步安装: 236 | 237 | ``` 238 | # 一步安装 239 | #ansible-playbook 90.setup.yml 240 | ``` 241 | 242 | 243 | 244 | ## 在k8s中运行一个程序 245 | 246 | ### 编写YAML文件 247 | 248 | Kubernetes 跟 Docker 等很多项目最大的不同,就在于它不推荐你使用命令行的方式直接运行容器,而是希望你用 YAML 文件的方式,然后用这样一句指令把它运行起来: 249 | 250 | ``` 251 | $ kubectl create -f 我的配置文件 252 | ``` 253 | 254 | yaml如下: 255 | 256 | ```yaml 257 | apiVersion: apps/v1 258 | kind: Deployment 259 | metadata: 260 | name: nginx-deployment 261 | spec: 262 | selector: 263 | matchLabels: 264 | app: nginx 265 | replicas: 2 266 | template: 267 | metadata: 268 | labels: 269 | app: nginx 270 | spec: 271 | containers: 272 | - name: nginx 273 | image: nginx:1.7.9 274 | ports: 275 | - containerPort: 80 276 | ``` 277 | 278 | Kind 字段,指定了这个 API 对象的类型(Type),是一个 Deployment。Deployment,是一个定义多副本应用的对象,Deployment 还负责在 Pod 定义发生变化时,对每个副本进行滚动更新。 279 | 280 | 像这样使用一种 API 对象(Deployment)管理另一种 API 对象(Pod)的方法,在Kubernetes 中,叫作“控制器”模式(controller pattern)。 281 | 282 | 一个 Kubernetes 的 API 对象的定义,大多可以分为 Metadata 和 Spec 两个部分。前者存放的是这个对象的元数据,对所有 API 对象来说,这一部分的字段和格式基本上是一样的;而后者存放的,则是属于这个对象独有的定义,用来描述它所要表达的功能。 283 | 284 | spec.replicas是2,表示定义的 Pod 副本个数。 285 | 286 | spec.template作用是描述了我想要创建的 Pod 的细节。spec.containers.image表示容器镜像是nginx:1.7.9,containerPort表示监听的端口是80。 287 | 288 | Labels 字段可以用来过滤控制对象,在上面这个 YAML 文件中,Deployment 会把所有正在运行的、携带“app: nginx”标签的 Pod 识别为被管理的对象,并确保这些 Pod 的总数严格等于两个。 289 | 290 | spec.selector.matchLabels是用来定义过滤规则的,一般称之为:Label Selector。 291 | 292 | ### 创建一个pod 293 | 294 | 在理解完上面的知识后,我们可以运行起来: 295 | 296 | ``` 297 | $ kubectl create -f nginx-deployment.yaml 298 | ``` 299 | 300 | 然后,通过 kubectl get 命令检查这个 YAML 运行起来的状态是不是与我们预期的一致: 301 | 302 | ``` 303 | $ kubectl get pods -l app=nginx 304 | NAME READY STATUS RESTARTS AGE 305 | nginx-deployment-67594d6bf6-9gdvr 1/1 Running 0 10m 306 | nginx-deployment-67594d6bf6-v6j7w 1/1 Running 0 10m 307 | ``` 308 | 309 | -l表示获取所有匹配 app: nginx 标签的 Pod。 310 | 311 | 注意:在命令行中,所有 key-value 格式的参数,都使用“=”而非“:”表示。 312 | 313 | 此外,我们还可以使用kubectl describe 命令,查看一个 API 对象的细节: 314 | 315 | ``` 316 | $ kubectl describe pod nginx-deployment-67594d6bf6-9gdvr 317 | Name: nginx-deployment-67594d6bf6-9gdvr 318 | Namespace: default 319 | Priority: 0 320 | PriorityClassName: 321 | Node: node-1/10.168.0.3 322 | Start Time: Thu, 16 Aug 2018 08:48:42 +0000 323 | Labels: app=nginx 324 | pod-template-hash=2315082692 325 | Annotations: 326 | Status: Running 327 | IP: 10.32.0.23 328 | Controlled By: ReplicaSet/nginx-deployment-67594d6bf6 329 | ... 330 | Events: 331 | 332 | Type Reason Age From Message 333 | 334 | ---- ------ ---- ---- ------- 335 | 336 | Normal Scheduled 1m default-scheduler Successfully assigned default/nginx-deployment-67594d6bf6-9gdvr to node-1 337 | Normal Pulling 25s kubelet, node-1 pulling image "nginx:1.7.9" 338 | Normal Pulled 17s kubelet, node-1 Successfully pulled image "nginx:1.7.9" 339 | Normal Created 17s kubelet, node-1 Created container 340 | Normal Started 17s kubelet, node-1 Started container 341 | ``` 342 | 343 | 上面详细信息中,在 Kubernetes 执行的过程中,对 API 对象的所有重要操作都会记录到Events里面。 344 | 345 | ### 为pod进行版本升级 346 | 347 | 如果我们要对这个 Nginx 服务进行升级,把它的镜像版本从 1.7.9 升级为 1.8,那么我们可以修改一下YAML文件: 348 | 349 | ```yaml 350 | ... 351 | spec: 352 | containers: 353 | - name: nginx 354 | image: nginx:1.8 #这里被从1.7.9修改为1.8 355 | ports: 356 | - containerPort: 80 357 | ``` 358 | 359 | 然后使用kubectl replace 指令来完成k8s集群中更新: 360 | 361 | ``` 362 | $ kubectl replace -f nginx-deployment.yaml 363 | ``` 364 | 365 | 我们也可以使用kubectl apply 命令,来统一进行 Kubernetes 对象的创建和更新操作,具体做法如下所示: 366 | 367 | ``` 368 | $ kubectl apply -f nginx-deployment.yaml 369 | 370 | # 修改nginx-deployment.yaml的内容 371 | 372 | $ kubectl apply -f nginx-deployment.yaml 373 | ``` 374 | 375 | 使用kubectl apply 命令是 Kubernetes“声明式 API”所推荐的使用方法。也就是说,作为用户,你不必关心当前的操作是创建,还是更新,你执行的命令始终是 kubectl apply,而 Kubernetes 则会根据 YAML 文件的内容变化,自动进行具体的处理。 376 | 377 | ### 声明一个 Volume 378 | 379 | 在 Kubernetes 中,Volume 是属于 Pod 对象的一部分。所以,我们就需要修改这个 YAML 文件里的 template.spec 字段,如下所示: 380 | 381 | ```yaml 382 | apiVersion: apps/v1 383 | kind: Deployment 384 | metadata: 385 | name: nginx-deployment 386 | spec: 387 | selector: 388 | matchLabels: 389 | app: nginx 390 | replicas: 2 391 | template: 392 | metadata: 393 | labels: 394 | app: nginx 395 | spec: 396 | containers: 397 | - name: nginx 398 | image: nginx:1.8 399 | ports: 400 | - containerPort: 80 401 | volumeMounts: 402 | - mountPath: "/usr/share/nginx/html" 403 | name: nginx-vol 404 | volumes: 405 | - name: nginx-vol 406 | emptyDir: {} 407 | ``` 408 | 409 | 我们在上面定义了volumes字段,名字叫做nginx-vol,类型是 emptyDir。Pod 中的容器,使用的是 volumeMounts 字段来声明自己要挂载哪个 Volume,并通过 mountPath 字段来定义容器内的 Volume 目录,比如:/usr/share/nginx/html。 410 | 411 | emptyDir表示,Kubernetes 也会在宿主机上创建一个临时目录,这个目录将来就会被绑定挂载到容器所声明的 Volume 目录上。 412 | 413 | 然后使用kubectl apply 指令,更新这个Deployment,然后使用 kubectl describe 查看一下最新的 Pod: 414 | 415 | ```yaml 416 | ... 417 | Containers: 418 | nginx: 419 | Container ID: docker://07b4f89248791c2aa47787e3da3cc94b48576cd173018356a6ec8db2b6041343 420 | Image: nginx:1.8 421 | ... 422 | Environment: 423 | Mounts: 424 | /usr/share/nginx/html from nginx-vol (rw) 425 | ... 426 | Volumes: 427 | nginx-vol: 428 | Type: EmptyDir (a temporary directory that shares a pod's lifetime) 429 | ``` 430 | 431 | 最后,你还可以使用 kubectl exec 指令,进入到这个 Pod 当中(即容器的 Namespace 中)查看这个 Volume 目录: 432 | 433 | ``` 434 | $ kubectl exec -it nginx-deployment-5c678cfb6d-lg9lw -- /bin/bash 435 | # ls /usr/share/nginx/html 436 | ``` 437 | 438 | 如果要删除这个Nginx Deployment 的话,直接执行: 439 | 440 | ``` 441 | $ kubectl delete -f nginx-deployment.yaml 442 | ``` 443 | 444 | ## 总结 445 | 446 | 1. 与k8s进行交互尽量选择yaml文件交互; 447 | 2. 我们可以使用kubectl create 命令创建一个pod; 448 | 3. 想要获取目前pod的状态可以使用kubectl get pods命令; 449 | 4. 使用kubectl describe pod 可以查看某个pod的详细信息; 450 | 5. 如果想要对pod更新最好使用kubectl apply -f ,这样可以做到更加无感的创建pod或更新; 451 | 6. 我们可以使用Volumes来挂载卷; 452 | 7. 使用kubectl delete -f可以删除一个pod; -------------------------------------------------------------------------------- /深入k8s/10.深入k8s:优先级及抢占机制源码分析/20200905190824.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/10.深入k8s:优先级及抢占机制源码分析/20200905190824.png -------------------------------------------------------------------------------- /深入k8s/10.深入k8s:优先级及抢占机制源码分析/20200912223315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/10.深入k8s:优先级及抢占机制源码分析/20200912223315.png -------------------------------------------------------------------------------- /深入k8s/10.深入k8s:优先级及抢占机制源码分析/20200912223345.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/10.深入k8s:优先级及抢占机制源码分析/20200912223345.png -------------------------------------------------------------------------------- /深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120525.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120525.png -------------------------------------------------------------------------------- /深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120529.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120529.png -------------------------------------------------------------------------------- /深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120534.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120534.png -------------------------------------------------------------------------------- /深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120537.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920120537.png -------------------------------------------------------------------------------- /深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920121011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/11.深入k8s:kubelet工作原理及其初始化源码分析/20200920121011.jpg -------------------------------------------------------------------------------- /深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200920120525.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200920120525.png -------------------------------------------------------------------------------- /深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200926201132.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200926201132.jpg -------------------------------------------------------------------------------- /深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200926201151.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200926201151.png -------------------------------------------------------------------------------- /深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200926201155.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/12.深入k8s:kubelet创建pod流程源码分析/20200926201155.png -------------------------------------------------------------------------------- /深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174855.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174855.jpg -------------------------------------------------------------------------------- /深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174900.png -------------------------------------------------------------------------------- /深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174910.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174910.png -------------------------------------------------------------------------------- /深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174924.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/13.深入k8s:Pod水平自动扩缩HPA及其源码分析/20201004174924.png -------------------------------------------------------------------------------- /深入k8s/14.深入k8s:kube-proxy代理ipvs及其源码分析/20201008173220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/14.深入k8s:kube-proxy代理ipvs及其源码分析/20201008173220.png -------------------------------------------------------------------------------- /深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223447.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223447.jpg -------------------------------------------------------------------------------- /深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223452.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223452.png -------------------------------------------------------------------------------- /深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223458.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223458.png -------------------------------------------------------------------------------- /深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223510.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/15.深入k8s:Event事件处理及其源码分析/20201011223510.png -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/63831060_p0_master1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/63831060_p0_master1200.jpg -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/71001144_p0_master1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/71001144_p0_master1200.jpg -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/image-20201017000513025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/image-20201017000513025.png -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/image-20201017000845410.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/image-20201017000845410.png -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/image-20201017213554429.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/image-20201017213554429.png -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/image-20201017213620364.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/image-20201017213620364.png -------------------------------------------------------------------------------- /深入k8s/16.深入k8s/image-20201017233145402.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/16.深入k8s/image-20201017233145402.png -------------------------------------------------------------------------------- /深入k8s/2.深入k8s:Pod对象中重要概念及用法/20200724234654.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/2.深入k8s:Pod对象中重要概念及用法/20200724234654.jpg -------------------------------------------------------------------------------- /深入k8s/3.深入k8s:Deployment控制器.md: -------------------------------------------------------------------------------- 1 | # 3.深入k8s:Deployment控制器 2 | 3 | ![01d4985f180a37a8012066214a289c.png@1280w_1l_2o_100sh](3.深入k8s:Deployment控制器/20200726183429.png) 4 | 5 | Deployment可以做到很便捷的管理Pod,只需要在Deployment中描述一下希望的Pod状态时什么,包括定义Pod副本数、滚动升级和回滚应用、扩容和缩容、暂停和继续Deployment等,然后Deployment Controller就可以帮我们实现我们想要达到的状态。 6 | 7 | 我们从一个例子入手: 8 | 9 | ```yaml 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: nginx-deployment 14 | spec: 15 | selector: 16 | matchLabels: 17 | app: nginx 18 | replicas: 2 19 | template: 20 | metadata: 21 | labels: 22 | app: nginx 23 | spec: 24 | containers: 25 | - name: nginx 26 | image: nginx:1.7.9 27 | ports: 28 | - containerPort: 80 29 | ``` 30 | 31 | 这个例子中: 32 | 33 | * 我们定义了一个Deployment,名字叫nginx-deployment; 34 | 35 | * 通过spec.replicas字段定义了Pod的副本数是2; 36 | 37 | * 通过spec.selector字段定义了被打上app: nginx的标签的Pod才会被管理; 38 | 39 | * tmplate字段定义了这个Deployment管理的Pod应该是怎样的,具有怎样的属性; 40 | 41 | 总的来说一个Deploymet控制器可以由两部分组成: 42 | 43 | ![img](3.深入k8s:Deployment控制器/72cc68d82237071898a1d149c8354b26.png) 44 | 45 | ### ReplicaSet 46 | 47 | ReplicaSet是一个副本控制器,ReplicaSet可以用selector来控制Pod的数量,而Deployments是一个更高层次的概念,它管理ReplicaSets,并提供对pod的声明性更新以及许多其他的功能。 48 | 49 | Deployment通过控制ReplicaSet的个数来和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。 50 | 51 | ### 水平扩展与滚动更新 52 | 53 | 我们可以通过下面这个命令来创建Deployment: 54 | 55 | ``` 56 | kubectl create -f nginx-deployment.yaml --record 57 | ``` 58 | 59 | 这里–record 参数作用是记录下你每次操作所执行的命令,以方便后面查看。 60 | 61 | 检查一下Deployment状态: 62 | 63 | ``` 64 | kubectl get deployments 65 | 66 | NAME READY UP-TO-DATE AVAILABLE AGE 67 | nginx-deployment 2/2 2 2 33m 68 | ``` 69 | 70 | * NAME:列举出Deployments 的名字; 71 | * READY:显示了多少个应用的副本是可用的,目前副本数/期望副本数; 72 | * UP-TO-DATE:显示了副本更新的次数,这里显示为2,说明已经做过2此更新; 73 | * AVAILABLE:显示了多少个应用的副本是可用的; 74 | * AGE:显示了应用运行的时间。 75 | 76 | 现在我们使用kubectl scale来做一个水平扩展: 77 | 78 | ``` 79 | kubectl scale deployment nginx-deployment --replicas=4 80 | 81 | deployment.apps/nginx-deployment scaled 82 | ``` 83 | 84 | 然后我们可以使用rollout status来查看滚动更新的状态: 85 | 86 | ``` 87 | kubectl rollout status deployment/nginx-deployment 88 | 89 | 90 | Waiting for deployment "nginx-deployment" rollout to finish: 2 of 4 updated replicas are available... 91 | Waiting for deployment "nginx-deployment" rollout to finish: 3 of 4 updated replicas are available... 92 | deployment "nginx-deployment" successfully rolled out 93 | ``` 94 | 95 | 上面的3 of 4 updated replicas are available表示已经有3个Pod进入了UP-TO-DATE 状态。 96 | 97 | 然后我们还可以查看一下ReplicaSet状态: 98 | 99 | ``` 100 | kubectl get rs 101 | 102 | NAME DESIRED CURRENT READY AGE 103 | nginx-deployment-9754ccbdf 4 4 4 44m 104 | ``` 105 | 106 | 在运行了一个Deployment修改后,Deployment Controller会创建一个副本为ReplicaSet,并会生成一串随机字符,ReplicaSet 会把这个随机字符串加在它所控制的所有 Pod 的标签里,从而保证这些 Pod 不会与集群里的其他 Pod 混淆。 107 | 108 | 我们可以通过 kubectl get pods --show-labels来查看: 109 | 110 | ``` 111 | NAME READY STATUS RESTARTS AGE LABELS 112 | nginx-deployment-9754ccbdf-5pl2j 1/1 Running 0 5m4s app=nginx,pod-template-hash=9754ccbdf 113 | nginx-deployment-9754ccbdf-67d4g 1/1 Running 0 48m app=nginx,pod-template-hash=9754ccbdf 114 | nginx-deployment-9754ccbdf-9drgb 1/1 Running 0 48m app=nginx,pod-template-hash=9754ccbdf 115 | nginx-deployment-9754ccbdf-fdmrx 1/1 Running 0 5m4s app=nginx,pod-template-hash=9754ccbdf 116 | ``` 117 | 118 | 下面我们看看Deployment的滚动更新是如何做的: 119 | 120 | 我们使用set image 来修改deployment中的镜像 121 | 122 | ``` 123 | kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1 --record 124 | 125 | deployment.apps/nginx-deployment image updated 126 | ``` 127 | 128 | 然后我们可以通过kubectl describe查看滚动更新的过程: 129 | 130 | ``` 131 | 132 | $ kubectl describe deployment nginx-deployment 133 | ... 134 | Events: 135 | Type Reason Age From Message 136 | ---- ------ ---- ---- ------- 137 | ... 138 | Normal ScalingReplicaSet 29s deployment-controller Scaled down replica set nginx-deployment-9754ccbdf to 3 139 | Normal ScalingReplicaSet 29s deployment-controller Scaled up replica set nginx-deployment-dc46b5ffc to 2 140 | Normal ScalingReplicaSet 12s (x2 over 11m) deployment-controller Scaled down replica set nginx-deployment-9754ccbdf to 2 141 | Normal ScalingReplicaSet 12s deployment-controller Scaled up replica set nginx-deployment-dc46b5ffc to 3 142 | Normal ScalingReplicaSet 11s deployment-controller Scaled down replica set nginx-deployment-9754ccbdf to 1 143 | Normal ScalingReplicaSet 11s deployment-controller Scaled up replica set nginx-deployment-dc46b5ffc to 4 144 | Normal ScalingReplicaSet 11s deployment-controller Scaled down replica set nginx-deployment-9754ccbdf to 0 145 | ``` 146 | 147 | 可以看到Deployment Controller控制ReplicaSet将旧的Pod 副本数一个个减少,并创建一个新的ReplicaSet:nginx-deployment-dc46b5ffc,并将其控制的Pod副本数一个个增加。 148 | 149 | 在这个“滚动更新”过程完成之后,你可以查看一下新、旧两个 ReplicaSet 的最终状态: 150 | 151 | ``` 152 | kubectl get rs 153 | 154 | NAME DESIRED CURRENT READY AGE 155 | nginx-deployment-9754ccbdf 0 0 0 57m 156 | nginx-deployment-dc46b5ffc 4 4 4 5m9s 157 | ``` 158 | 159 | 默认的情况下,Deployment 会保至少有75%的Pod还是可用的,所以我们的例子中,会保证至少有3个Pod是出于可用的状态。 160 | 161 | 这个策略,是 Deployment 对象的一个字段,名叫 RollingUpdateStrategy,如下所示: 162 | 163 | ``` 164 | kubectl describe deployment 165 | 166 | 167 | Name: nginx-deployment 168 | ... 169 | Selector: app=nginx 170 | Replicas: 4 desired | 4 updated | 4 total | 4 available | 0 unavailable 171 | StrategyType: RollingUpdate 172 | MinReadySeconds: 0 173 | RollingUpdateStrategy: 25% max unavailable, 25% max surge 174 | ... 175 | ``` 176 | 177 | 25% max unavailable表示最大25%不可用;25% max surge表示最多可以创建多少个新的Pod。 178 | 179 | ### 版本控制与回滚 180 | 181 | 在上面的操作中,我们将nginx的版本设置成了1.16.1,现在我们可以通过rollout undo来进行版本的回滚: 182 | 183 | ``` 184 | kubectl rollout undo deployment/nginx-deployment 185 | 186 | deployment.apps/nginx-deployment rolled back 187 | ``` 188 | 189 | 190 | 191 | 当然除了这个命令以外由于我们使用了--record,所以我们可以通过kubectl rollout history 来查看版本信息: 192 | 193 | ``` 194 | kubectl rollout history deployment/nginx-deployment 195 | 196 | deployment.apps/nginx-deployment 197 | REVISION CHANGE-CAUSE 198 | 2 kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1 --record=true 199 | 3 kubectl apply --filename=nginx-deployment.yaml --record=true 200 | ``` 201 | 202 | 我们可以通过kubectl rollout history来看到相应版本的具体信息: 203 | 204 | ``` 205 | kubectl rollout history deployment/nginx-deployment --revision=2 206 | ``` 207 | 208 | 通过kubectl rollout undo命令来回滚到相应的版本: 209 | 210 | ``` 211 | kubectl rollout undo deployment/nginx-deployment --to-revision=2 212 | ``` 213 | 214 | ### Pausing 和Resuming 一个Deployment 215 | 216 | 我们在上面的操作中,每进行一步操作都会进行一次滚动更新,如果我们想一次性执行多条命令然后再一次性滚动更新,那么可以先pause Deployment然后操作完之后再resume 它。 217 | 218 | 如下: 219 | 220 | ``` 221 | kubectl rollout pause deployment/nginx-deployment 222 | 223 | deployment.apps/nginx-deployment paused 224 | ``` 225 | 226 | 然后就可以随意的修改这个 Deployment 的内容了。由于此时 Deployment 正处于“暂停”状态,所以我们对 Deployment 的所有修改,都不会触发新的“滚动更新”,也不会创建新的 ReplicaSet。 227 | 228 | 229 | 230 | 操作完之后再执行: 231 | 232 | ``` 233 | kubectl rollout resume deployment/nginx-deployment 234 | 235 | deployment.apps/nginx-deployment resumed 236 | ``` 237 | 238 | 在 kubectl rollout pause 指令之后的这段时间里,我们对 Deployment 进行的所有修改,最后只会触发一次“滚动更新”。 239 | 240 | -------------------------------------------------------------------------------- /深入k8s/3.深入k8s:Deployment控制器/20200726183429.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/3.深入k8s:Deployment控制器/20200726183429.png -------------------------------------------------------------------------------- /深入k8s/3.深入k8s:Deployment控制器/711c07208358208e91fa7803ebc73058.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/3.深入k8s:Deployment控制器/711c07208358208e91fa7803ebc73058.jpg -------------------------------------------------------------------------------- /深入k8s/3.深入k8s:Deployment控制器/72cc68d82237071898a1d149c8354b26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/3.深入k8s:Deployment控制器/72cc68d82237071898a1d149c8354b26.png -------------------------------------------------------------------------------- /深入k8s/4.深入k8s:容器持久化存储/01723d5f23d906a801215aa0f293ac.jpg@1280w_1l_2o_100sh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/4.深入k8s:容器持久化存储/01723d5f23d906a801215aa0f293ac.jpg@1280w_1l_2o_100sh.jpg -------------------------------------------------------------------------------- /深入k8s/4.深入k8s:容器持久化存储/pv_and_pvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/4.深入k8s:容器持久化存储/pv_and_pvc.png -------------------------------------------------------------------------------- /深入k8s/5.深入k8s:StatefulSet控制器/StatefulSet A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/5.深入k8s:StatefulSet控制器/StatefulSet A.png -------------------------------------------------------------------------------- /深入k8s/5.深入k8s:StatefulSet控制器/image-20200807220814361.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/5.深入k8s:StatefulSet控制器/image-20200807220814361.png -------------------------------------------------------------------------------- /深入k8s/5.深入k8s:StatefulSet控制器/发生故障.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/5.深入k8s:StatefulSet控制器/发生故障.png -------------------------------------------------------------------------------- /深入k8s/6.深入k8s:守护进程DaemonSet及其源码分析.md: -------------------------------------------------------------------------------- 1 | # 6.深入k8s:守护进程DaemonSet及其源码分析 2 | 3 | > 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 4 | 5 | ![img](6.深入k8s:守护进程DaemonSet及其源码分析/20200809185827.jpg) 6 | 7 | 最近也一直在加班,处理项目中的事情,发现问题越多越是感觉自己的能力不足,希望自己能多学点。我觉得人生的意义就是在于能够不断的寻求突破吧。 8 | 9 | 这篇文章会讲DaemonSet和Job与CronJob一起。在讲其中某一块内容的时候,我会将一些其他内容也关联上,让读者尽可能的看明白些,然后这篇开始我会开始加入一些主要源码的分析。 10 | 11 | 如果觉得我讲的不错的,可以发个邮件鼓励一下我噢~ 12 | 13 | 14 | 15 | Daemon Pod有三个主要特征: 16 | 17 | 1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上; 18 | 2. 每个节点上只有一个这样的 Pod 实例; 19 | 3. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。 20 | 21 | Daemon Pod可以运用在网络插件的Agent组件上、日志组件、监控组件等。 22 | 23 | ### 创建一个DaemonSet 24 | 25 | ```yaml 26 | apiVersion: apps/v1 27 | kind: DaemonSet 28 | metadata: 29 | name: fluentd-elasticsearch 30 | namespace: kube-system 31 | labels: 32 | k8s-app: fluentd-logging 33 | spec: 34 | selector: 35 | matchLabels: 36 | name: fluentd-elasticsearch 37 | template: 38 | metadata: 39 | labels: 40 | name: fluentd-elasticsearch 41 | spec: 42 | tolerations: 43 | - key: node-role.kubernetes.io/master 44 | effect: NoSchedule 45 | containers: 46 | - name: fluentd-elasticsearch 47 | image: mirrorgooglecontainers/fluentd-elasticsearch:v2.4.0 48 | resources: 49 | limits: 50 | memory: 200Mi 51 | requests: 52 | cpu: 100m 53 | memory: 200Mi 54 | volumeMounts: 55 | - name: varlog 56 | mountPath: /var/log 57 | - name: varlibdockercontainers 58 | mountPath: /var/lib/docker/containers 59 | readOnly: true 60 | terminationGracePeriodSeconds: 30 61 | volumes: 62 | - name: varlog 63 | hostPath: 64 | path: /var/log 65 | - name: varlibdockercontainers 66 | hostPath: 67 | path: /var/lib/docker/containers 68 | ``` 69 | 70 | 这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod。通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch 中。 71 | 72 | 这个DaemonSet中使用 selector 选择管理所有携带了 name=fluentd-elasticsearch 标签的 Pod。然后使用template定义了pod模板。 73 | 74 | 75 | 76 | 然后在运行这个DaemonSet后,一个叫DaemonSet Controller的控制器会从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。然后检查Node上是不是又name=fluentd-elasticsearch 标签的 Pod 在运行。 77 | 78 | 如果没有这样的pod,那么就创建一个这样的pod;如果node上这样的pod数量大于1,那么就会删除多余的pod。 79 | 80 | 运行: 81 | 82 | ```shell 83 | $ kubectl apply -f ds-els.yaml 84 | ``` 85 | 86 | 然后查看运行情况: 87 | 88 | ```shell 89 | $ kubectl get pod -n kube-system -l name=fluentd-elasticsearch 90 | 91 | NAME READY STATUS RESTARTS AGE 92 | fluentd-elasticsearch-nwqph 1/1 Running 0 4m11s 93 | ``` 94 | 95 | 由于我这是单节点,所以只有一个pod运行了。 96 | 97 | 然后查看一下 Kubernetes 集群里的 DaemonSet 对象: 98 | 99 | ```shell 100 | $ kubectl get ds -n kube-system fluentd-elasticsearch 101 | NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE 102 | fluentd-elasticsearch 1 1 1 1 1 27m 103 | ``` 104 | 105 | 106 | 107 | 然后我们来稍微看一下源码,k8s是通过daemon_controller里面的manage方法来管理Pod删减操作的: 108 | 109 | manage方法里面首先会获取daemon pod 与 node 的映射关系,然后判断每一个 node 是否需要运行 daemon pod,然后遍历完node之后将需要创建的Pod列表和需要删除Pod的列表交给syncNodes执行。 110 | 111 | ```go 112 | func (dsc *DaemonSetsController) manage(ds *apps.DaemonSet, nodeList []*v1.Node, hash string) error { 113 | // 获取已存在 daemon pod 与 node 的映射关系 114 | nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds) 115 | if err != nil { 116 | return fmt.Errorf("couldn't get node to daemon pod mapping for daemon set %q: %v", ds.Name, err) 117 | } 118 | 119 | // 判断每一个 node 是否需要运行 daemon pod 120 | var nodesNeedingDaemonPods, podsToDelete []string 121 | for _, node := range nodeList { 122 | nodesNeedingDaemonPodsOnNode, podsToDeleteOnNode, err := dsc.podsShouldBeOnNode( 123 | node, nodeToDaemonPods, ds) 124 | 125 | if err != nil { 126 | continue 127 | } 128 | //将需要删除的Pod和需要在某个节点创建Pod存入列表中 129 | nodesNeedingDaemonPods = append(nodesNeedingDaemonPods, nodesNeedingDaemonPodsOnNode...) 130 | podsToDelete = append(podsToDelete, podsToDeleteOnNode...) 131 | } 132 | 133 | podsToDelete = append(podsToDelete, getUnscheduledPodsWithoutNode(nodeList, nodeToDaemonPods)...) 134 | 135 | //为对应的 node 创建 daemon pod 以及删除多余的 pods 136 | if err = dsc.syncNodes(ds, podsToDelete, nodesNeedingDaemonPods, hash); err != nil { 137 | return err 138 | } 139 | 140 | return nil 141 | } 142 | ``` 143 | 144 | 145 | 146 | 下面我们看一下podsShouldBeOnNode方法是如何判断哪些Pod需要创建和删除的: 147 | 148 | 在podsShouldBeOnNode会调用nodeShouldRunDaemonPod方法来判断该node是否需要运行 daemon pod 以及能不能调度成功,然后获取该node上有没有创建该daemon pod。 149 | 150 | 通过判断shouldRun, shouldContinueRunning将需要创建 daemon pod 的 node 列表以及需要删除的 pod 列表获取到,shouldSchedule 主要检查 node 上的资源是否充足,shouldContinueRunning 默认为 true。 151 | 152 | ```go 153 | func (dsc *DaemonSetsController) podsShouldBeOnNode( 154 | node *v1.Node, 155 | nodeToDaemonPods map[string][]*v1.Pod, 156 | ds *apps.DaemonSet, 157 | ) (nodesNeedingDaemonPods, podsToDelete []string, err error) { 158 | //判断该 node 是否需要运行 daemon pod 以及能不能调度成功 159 | shouldRun, shouldContinueRunning, err := dsc.nodeShouldRunDaemonPod(node, ds) 160 | if err != nil { 161 | return 162 | } 163 | //获取该节点上的指定ds的pod列表 164 | daemonPods, exists := nodeToDaemonPods[node.Name] 165 | 166 | switch { 167 | //如果daemon pod是可以运行在这个node上,但是还没有创建,那么创建一个 168 | case shouldRun && !exists: 169 | nodesNeedingDaemonPods = append(nodesNeedingDaemonPods, node.Name) 170 | // 需要 pod 一直运行 171 | case shouldContinueRunning: 172 | var daemonPodsRunning []*v1.Pod 173 | for _, pod := range daemonPods { 174 | if pod.DeletionTimestamp != nil { 175 | continue 176 | } 177 | //如果 pod 运行状态为 failed,则删除该 pod 178 | if pod.Status.Phase == v1.PodFailed { 179 | ... 180 | podsToDelete = append(podsToDelete, pod.Name) 181 | } else { 182 | daemonPodsRunning = append(daemonPodsRunning, pod) 183 | } 184 | } 185 | //如果节点上已经运行 daemon pod 数 > 1,保留运行时间最长的 pod,其余的删除 186 | if len(daemonPodsRunning) > 1 { 187 | sort.Sort(podByCreationTimestampAndPhase(daemonPodsRunning)) 188 | for i := 1; i < len(daemonPodsRunning); i++ { 189 | podsToDelete = append(podsToDelete, daemonPodsRunning[i].Name) 190 | } 191 | } 192 | // 如果 pod 不需要继续运行但 pod 已存在则需要删除 pod 193 | case !shouldContinueRunning && exists: 194 | for _, pod := range daemonPods { 195 | if pod.DeletionTimestamp != nil { 196 | continue 197 | } 198 | podsToDelete = append(podsToDelete, pod.Name) 199 | } 200 | } 201 | 202 | return nodesNeedingDaemonPods, podsToDelete, nil 203 | } 204 | ``` 205 | 206 | 207 | 208 | DaemonSet 对象的滚动更新和StatefulSet是一样的,可以通过 `.spec.updateStrategy.type` 设置更新策略。目前支持两种策略: 209 | 210 | * OnDelete:默认策略,更新模板后,只有手动删除了旧的 Pod 后才会创建新的 Pod; 211 | * RollingUpdate:更新 DaemonSet 模版后,自动删除旧的 Pod 并创建新的 Pod。 212 | 213 | 具体的滚动更新可以在:[5.深入k8s:StatefulSet控制器](https://www.luozhiyun.com/archives/342)回顾一下。 214 | 215 | ### 仅在某些节点上运行 Pod 216 | 217 | 如果想让DaemonSet在某个特定的Node上运行,可以使用nodeAffinity。 218 | 219 | 如下: 220 | 221 | ```yaml 222 | apiVersion: v1 223 | kind: Pod 224 | metadata: 225 | name: with-node-affinity 226 | spec: 227 | affinity: 228 | nodeAffinity: 229 | requiredDuringSchedulingIgnoredDuringExecution: 230 | nodeSelectorTerms: 231 | - matchExpressions: 232 | - key: metadata.name 233 | operator: In 234 | values: 235 | - node1 236 | ``` 237 | 238 | 上面的这个pod,我们指定了nodeAffinity,matchExpressions的含义是这个pod只能运行在metadata.name是node1的节点上,operator=In表示部分匹配的意思,除此之外operator还可以指定:In,NotIn,Exists,DoesNotExist,Gt,Lt等。 239 | 240 | requiredDuringSchedulingIgnoredDuringExecution表明将pod调度到一个节点必须要满足的规则。除了这个规则还有preferredDuringSchedulingIgnoredDuringExecution将pod调度到一个节点可能不会满足规则 241 | 242 | 243 | 244 | 当我们使用如下命令的时候: 245 | 246 | ```shell 247 | $ kubectl edit pod -n kube-system fluentd-elasticsearch-nwqph 248 | 249 | ... 250 | spec: 251 | affinity: 252 | nodeAffinity: 253 | requiredDuringSchedulingIgnoredDuringExecution: 254 | nodeSelectorTerms: 255 | - matchFields: 256 | - key: metadata.name 257 | operator: In 258 | values: 259 | - node1 260 | ... 261 | ``` 262 | 263 | 可以看到DaemonSet自动帮我们加上了affinity来进行节点调度。我们也可以自己在yaml里面设置affinity,以此来覆盖系统默认的配置。 264 | 265 | 266 | 267 | ### Taints and Tolerations 268 | 269 | 在k8s集群中,我们可以给Node打上污点,这样可以让pod避开那些不合适的node。在node上设置一个或多个Taint后,除非pod明确声明能够容忍这些污点,否则无法在这些node上运行。 270 | 271 | 272 | 273 | 例如: 274 | 275 | ``` 276 | kubectl taint nodes node1 key=value:NoSchedule 277 | ``` 278 | 279 | 上面给node1打上了一个污点,这将阻止pod调度到node1这个节点上。 280 | 281 | 如果要移除这个污点,可以这么做: 282 | 283 | ``` 284 | kubectl taint nodes node1 key:NoSchedule- 285 | ``` 286 | 287 | 288 | 289 | 如果我们想让pod运行在有污点的node节点上,我们需要在pod上声明Toleration,表明可以容忍具有该Taint的Node。 290 | 291 | 比如我们可以声明如下pod: 292 | 293 | ```yaml 294 | apiVersion: v1 295 | kind: Pod 296 | metadata: 297 | name: pod-taints 298 | spec: 299 | tolerations: 300 | - key: "key" 301 | operator: "Equal" 302 | value: "value" 303 | effect: "NoSchedule" 304 | containers: 305 | - name: pod-taints 306 | image: busybox:latest 307 | ``` 308 | 309 | operator在这里可以是Exists表示无需指定value,值为Equal表明需要指明和value相等。 310 | 311 | NoSchedule表示如果一个pod没有声明容忍这个Taint,则系统不会把该Pod调度到有这个Taint的node上。除了NoSchedule外,还可以是PreferNoSchedule,表明如果一个Pod没有声明容忍这个Taint,则系统会尽量避免把这个pod调度到这一节点上去,但不是强制的。 312 | 313 | 314 | 315 | 在上面的fluentd-elasticsearch DaemonSet 里,我们加上了 316 | 317 | ``` 318 | tolerations: 319 | - key: node-role.kubernetes.io/master 320 | effect: NoSchedule 321 | ``` 322 | 323 | 是因为在默认情况下,Kubernetes 集群不允许用户在 Master 节点部署 Pod。因为,Master 节点默认携带了一个叫作node-role.kubernetes.io/master的“污点”。所以,为了能在 Master 节点上部署 DaemonSet 的 Pod,我就必须让这个 Pod“容忍”这个“污点”。 324 | 325 | ### Reference 326 | 327 | https://www.cnblogs.com/breezey/p/9101677.html 328 | 329 | https://kubernetes.io/docs/concepts/workloads/controllers/daemonset 330 | 331 | https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ 332 | 333 | https://kuboard.cn/learning/k8s-intermediate/workload/wl-daemonset/ 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /深入k8s/6.深入k8s:守护进程DaemonSet及其源码分析/20200809185827.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/6.深入k8s:守护进程DaemonSet及其源码分析/20200809185827.jpg -------------------------------------------------------------------------------- /深入k8s/7.深入k8s:任务调用Job与CronJob及源码分析/20200823163612.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/7.深入k8s:任务调用Job与CronJob及源码分析/20200823163612.png -------------------------------------------------------------------------------- /深入k8s/8.深入k8s:资源控制Qos和eviction及其源码分析/20200829221848.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/8.深入k8s:资源控制Qos和eviction及其源码分析/20200829221848.jpg -------------------------------------------------------------------------------- /深入k8s/9.深入k8s:调度器及其源码分析/20200905191205.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/9.深入k8s:调度器及其源码分析/20200905191205.jpg -------------------------------------------------------------------------------- /深入k8s/9.深入k8s:调度器及其源码分析/scheduler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/9.深入k8s:调度器及其源码分析/scheduler.png -------------------------------------------------------------------------------- /深入k8s/9.深入k8s:调度器及其源码分析/调度流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/9.深入k8s:调度器及其源码分析/调度流程.png -------------------------------------------------------------------------------- /深入k8s/Docker容器实现原理.md: -------------------------------------------------------------------------------- 1 | # Docker容器实现原理 2 | 3 | ### 容器中的进程隔离 4 | 5 | 容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。在Docker中使用了Namespace 技术来修改进程视图从而达到进程隔离的目的。 6 | 7 | 8 | 9 | 首先创建一个容器作为例子: 10 | 11 | ``` 12 | $ docker run -it busybox /bin/sh 13 | / # 14 | ``` 15 | 16 | -it 参数告诉了 Docker 项目在启动容器后,需要给我们分配一个文本输入 / 输出环境,也就是 TTY,跟容器的标准输入相关联,这样我们就可以和这个 Docker 容器进行交互了。而 /bin/sh 就是我们要在 Docker 容器里运行的程序。 17 | 18 | 19 | 20 | 如果我们执行如下命令: 21 | 22 | ``` 23 | / # ps 24 | PID USER TIME COMMAND 25 | 1 root 0:00 /bin/sh 26 | 10 root 0:00 ps 27 | ``` 28 | 29 | 可以看到,我们在 Docker 里最开始执行的 /bin/sh,就是这个容器内部的第 1 号进程(PID=1)。这就意味着,前面执行的 /bin/sh,以及我们刚刚执行的 ps,已经被 Docker 隔离在了一个跟宿主机完全不同的世界当中。 30 | 31 | 32 | 33 | 本来,每当我们在宿主机上运行了一个 /bin/sh 程序,操作系统都会给它分配一个进程编号,比如 PID=100。而现在,我们要通过 Docker 把这个 /bin/sh 程序运行在一个容器当中。 34 | 35 | 36 | 37 | Docker会将宿主机的操作系统里,还是原来的第 100 号进程通过Linux 里面的 Namespace 机制重新进行进程编号。如下: 38 | 39 | ``` 40 | int pid = clone(main_function, stack_size, SIGCHLD, NULL); 41 | ``` 42 | 43 | 当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如: 44 | 45 | ``` 46 | int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 47 | ``` 48 | 49 | 新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。 50 | 51 | 52 | 53 | 每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。 54 | 55 | 56 | 57 | 除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。 58 | 59 | 60 | 61 | 但是,基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。 62 | 63 | 首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。 64 | 65 | 66 | 67 | 其次,在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。 68 | 69 | 这就意味着,如果你的容器中的程序使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期。 70 | 71 | 72 | 73 | ### 容器中隔离中的资源限制 74 | 75 | Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。 76 | 77 | Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。 78 | 79 | 此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。 80 | 81 | 82 | 83 | 在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。用 mount 指令把它们展示出来,这条命令是: 84 | 85 | ``` 86 | $ mount -t cgroup 87 | cpuset on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) 88 | cpu on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu) 89 | cpuacct on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct) 90 | blkio on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) 91 | memory on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) 92 | ... 93 | ``` 94 | 95 | 可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。 96 | 97 | 比如,对 CPU 子系统来说,我们就可以看到如下几个配置文件,这个指令是: 98 | 99 | ``` 100 | $ ls /sys/fs/cgroup/cpu 101 | cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release 102 | cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks 103 | ``` 104 | 105 | 106 | 107 | 下面我们来使用一下cgroup,看看它是如何限制CPU的使用率的。 108 | 109 | 现在进入 /sys/fs/cgroup/cpu 目录下: 110 | 111 | ``` 112 | root@ubuntu:/sys/fs/cgroup/cpu$ mkdir container 113 | root@ubuntu:/sys/fs/cgroup/cpu$ ls container/ 114 | cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release 115 | cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks 116 | ``` 117 | 118 | 这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。 119 | 120 | 121 | 122 | 现在,我们在后台执行这样一条脚本: 123 | 124 | ```shell 125 | $ while : ; do : ; done & 126 | [1] 226 127 | ``` 128 | 129 | 显然,它执行了一个死循环,可以把计算机的 CPU 吃到 100%,根据它的输出,我们可以看到这个脚本在后台运行的进程号(PID)是 226。 130 | 131 | 这样,我们可以用 top 指令来确认一下 CPU 有没有被打满: 132 | 133 | ``` 134 | $ top 135 | %Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 136 | ``` 137 | 138 | 139 | 140 | 下面我们进入到container,看到 container 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us): 141 | 142 | ``` 143 | $ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 144 | -1 145 | $ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us 146 | 100000 147 | ``` 148 | 149 | 150 | 151 | 接下来,我们可以通过修改这些文件的内容来设置限制。比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us): 152 | 153 | ``` 154 | $ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 155 | ``` 156 | 157 | 它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。 158 | 159 | 160 | 161 | 接下来,我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了: 162 | 163 | ``` 164 | $ echo 226 > /sys/fs/cgroup/cpu/container/tasks 165 | ``` 166 | 167 | 我们可以用 top 指令查看一下: 168 | 169 | ``` 170 | $ top 171 | %Cpu0 : 20.3 us, 0.0 sy, 0.0 ni, 79.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 172 | ``` 173 | 174 | 可以看到,计算机的 CPU 使用率立刻降到了 20%。 175 | 176 | 177 | 178 | 对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。 179 | 180 | 想要了解更多的信息,可以看这篇:[Linux 资源管理指南](https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/6/html-single/resource_management_guide/index#idm140538582929664) 181 | 182 | 183 | 184 | ### 容器中隔离中的文件系统 185 | 186 | 如果一个容器需要启动,那么它一定需要提供一个根文件系统(rootfs),容器需要使用这个文件系统来创建一个新的进程,所有二进制的执行都必须在这个根文件系统中。 187 | 188 | 189 | 190 | 一个最常见的 rootfs,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等: 191 | 192 | ``` 193 | $ ls / 194 | bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var 195 | ``` 196 | 197 | 198 | 199 | 为了保证当前的容器进程没有办法访问宿主机器上其他目录,我们在这里还需要通过 libcontainer 提供的 pivot_root 或者 chroot 函数改变进程能够访问个文件目录的根节点。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。 200 | 201 | 202 | 203 | 通过pivot_root或chroot将容器需要的目录挂载到了容器中,同时也禁止当前的容器进程访问宿主机器上的其他目录,保证了不同文件系统的隔离。 204 | 205 | 206 | 207 | 但是rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。 208 | 209 | 210 | 211 | 这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。 212 | 213 | 214 | 215 | 我们首先来解释一下,什么是Mount Namespace: 216 | 217 | Mount Namespace用来隔离文件系统的挂载点,这样进程就只能看到自己的 mount namespace 中的文件系统挂载点。 218 | 219 | 进程的Mount Namespace中的挂载点信息可以在 /proc/[pid]/mounts、/proc/[pid]/mountinfo 和 /proc/[pid]/mountstats 这三个文件中找到。 220 | 221 | 222 | 223 | 然后我们再来看看什么是根文件系统rootfs: 224 | 225 | 根文件系统首先是一种文件系统,该文件系统不仅具有普通文件系统的存储数据文件的功能,但是相对于普通的文件系统,它的特殊之处在于,它是内核启动时所挂载(mount)的第一个文件系统,内核代码的映像文件保存在根文件系统中,系统引导启动程序会在根文件系统挂载之后从中把一些初始化脚本(如rcS,inittab)和服务加载到内存中去运行。 226 | 227 | 228 | 229 | Linux启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。成功之后可以自动或手动挂载其他的文件系统。 230 | 231 | 基于上面两个基础知识,我们知道一个Linux容器,首先应该要有一个文件隔离环境,并且还要实现rootfs。 232 | 233 | 而在 Linux 操作系统里,有一个名为 chroot 的命令可以实现改变进程的根目录到指定的位置的目的从而实现rootfs。 234 | 235 | 所以我们的容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以就创建了一个独立的隔离环境。 236 | 237 | 238 | 239 | 而挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统就是叫做rootfs。 240 | 241 | 所以,一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等: 242 | 243 | ``` 244 | $ ls / 245 | bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var 246 | ``` 247 | 248 | 249 | 250 | 由于有了rootfs之后,所以rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。这也就为容器镜像提供了“打包操作系统”的能力。 251 | 252 | #### 层(layer) 253 | 254 | Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。 255 | 256 | layer是使用了一种叫作联合文件系统(Union File System)的能力。 257 | 258 | 259 | 260 | Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录 A 和 B,它们分别有两个文件: 261 | 262 | ``` 263 | $ tree 264 | . 265 | ├── A 266 | │ ├── a 267 | │ └── x 268 | └── B 269 | ├── b 270 | └── x 271 | ``` 272 | 273 | 使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上: 274 | 275 | ``` 276 | $ mkdir C 277 | $ mount -t aufs -o dirs=./A:./B none ./C 278 | 279 | 280 | $ tree ./C 281 | ./C 282 | ├── a 283 | ├── b 284 | └── x 285 | ``` 286 | 287 | 比如我们拉取一个镜像: 288 | 289 | ``` 290 | $ docker run -d ubuntu:latest sleep 3600 291 | ``` 292 | 293 | 在Docker中,这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。但是Docker 镜像使用的 rootfs,往往由多个“层”组成: 294 | 295 | ``` 296 | $ docker image inspect ubuntu:latest 297 | ... 298 | "RootFS": { 299 | "Type": "layers", 300 | "Layers": [ 301 | "sha256:f49017d4d5ce9c0f544c...", 302 | "sha256:8f2b771487e9d6354080...", 303 | "sha256:ccd4d61916aaa2159429...", 304 | "sha256:c01d74f99de40e097c73...", 305 | "sha256:268a067217b5fe78e000..." 306 | ] 307 | } 308 | ``` 309 | 310 | 可以看到,这个 Ubuntu 镜像,实际上由五个层组成。这五个层就是五个增量 rootfs,每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上。 311 | 312 | 这个挂载点就是 /var/lib/docker/aufs/mnt/,比如,这个目录里面正是一个完整的 Ubuntu 操作系统: 313 | 314 | ``` 315 | $ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e 316 | bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var 317 | ``` 318 | 319 | 我们可以在/sys/fs/aufs 下查看被联合挂载在一起的各个层的信息: 320 | 321 | ``` 322 | $ cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]* 323 | /var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw 324 | /var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh 325 | /var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh 326 | /var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh 327 | /var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh 328 | /var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh 329 | /var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh 330 | ``` 331 | 332 | 从这个结构可以看出来,这个容器的 rootfs 由如下图所示的三部分组成: 333 | 334 | ![img](Docker容器实现原理/8a7b5cfabaab2d877a1d4566961edd5f.png) 335 | 336 | **第一部分,只读层。** 337 | 338 | 对应的正是 ubuntu:latest 镜像的五层。它们的挂载方式都是只读(ro+wh)的。 339 | 340 | ``` 341 | $ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0... 342 | etc sbin usr var 343 | $ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2... 344 | run 345 | $ ls /var/lib/docker/aufs/diff/a524a729adadedb900... 346 | bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var 347 | ``` 348 | 349 | **第二部分,可读写层。** 350 | 351 | 它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。 352 | 353 | 354 | 355 | 如果删除一个只读的文件,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。 356 | 357 | 比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。 358 | 359 | 360 | 361 | 最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。 362 | 363 | **第三部分,Init 层。** 364 | 365 | 它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。 366 | 367 | 需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。 368 | 369 | 但是这些修改往往只对当前的容器有效,并不会执行 docker commit 时,把这些信息连同可读写层一起提交掉。 370 | 371 | 372 | 373 | ![docker-filesystems](Docker容器实现原理/2017-11-30-docker-filesystems.png) 374 | 375 | 上面的这张图片非常好的展示了组装的过程,每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在一些底层服务(Kernel)上,包括命名空间、控制组、rootfs 等等,这种容器的组装方式提供了非常大的灵活性,只读的镜像层通过共享也能够减少磁盘的占用。 376 | 377 | 378 | 379 | 在最新的 Docker 中,overlay2 取代了 aufs 成为了推荐的存储驱动,但是在没有 overlay2 驱动的机器上仍然会使用 aufs作为 Docker 的默认驱动。 380 | 381 | 这篇官方文章里详细的介绍了存储驱动:[Docker storage drivers](https://docs.docker.com/storage/storagedriver/select-storage-driver/)。 382 | 383 | ### Docker exec的实现原理 384 | 385 | 比如说我们运行了一个Docker容器,我们如果想进入到容器内部进行操作,一般会使用如下命令: 386 | 387 | ``` 388 | docker exec -it {container id} /bin/sh 389 | ``` 390 | 391 | 通过使用docker exec 命令进入到了容器当中,那么docker exec 是怎么做到进入容器里的呢? 392 | 393 | 394 | 395 | 实际上,Linux Namespace 创建的隔离空间虽然看不见摸不着,但一个进程的 Namespace 信息在宿主机上是确确实实存在的,并且是以一个文件的方式存在。 396 | 397 | 398 | 399 | 比如,通过如下指令,你可以看到当前正在运行的 Docker 容器的进程号(PID): 400 | 401 | ``` 402 | # docker inspect --format '{{ .State.Pid }}' 6e27dcd23489 403 | 29659 404 | ``` 405 | 406 | 这时,你可以通过查看宿主机的 proc 文件,看到这个 29659 进程的所有 Namespace 对应的文件: 407 | 408 | ``` 409 | (base) [root@VM_243_186_centos ~]# ls -l /proc/29659/ns 410 | 总用量 0 411 | lrwxrwxrwx 1 root root 0 7月 14 15:18 cgroup -> cgroup:[4026531835] 412 | lrwxrwxrwx 1 root root 0 7月 14 15:18 ipc -> ipc:[4026532327] 413 | lrwxrwxrwx 1 root root 0 7月 14 15:09 mnt -> mnt:[4026532325] 414 | lrwxrwxrwx 1 root root 0 7月 14 15:09 net -> net:[4026532330] 415 | lrwxrwxrwx 1 root root 0 7月 14 15:18 pid -> pid:[4026532328] 416 | lrwxrwxrwx 1 root root 0 7月 14 15:18 uts -> uts:[4026532326] 417 | ``` 418 | 419 | 这也就意味着:一个进程,可以选择加入到某个进程已有的 Namespace 当中,从而达到“进入”这个进程所在容器的目的,这正是 docker exec 的实现原理。 420 | 421 | #### setns() 函数 422 | 423 | 通过 setns() 函数可以将当前进程加入到已有的 namespace 中。setns() 在 C 语言库中的声明如下: 424 | 425 | ``` 426 | #define _GNU_SOURCE 427 | #include 428 | int setns(int fd, int nstype); 429 | ``` 430 | 431 | * fd:表示要加入 namespace 的文件描述符。 432 | * nstype:参数 nstype 让调用者可以检查 fd 指向的 namespace 类型是否符合实际要求。若把该参数设置为 0 表示不检查。 433 | 434 | 435 | 436 | 所以说docker exec 这个操作背后,其实是利用了setns调用进入到 namespace从而进行相关的操作。 437 | 438 | #### Volume 机制 439 | 440 | Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作。 441 | 442 | 在 Docker 项目里,它支持两种 Volume 声明方式,可以把宿主机目录挂载进容器的 /test 目录当中: 443 | 444 | ``` 445 | $ docker run -v /test ... 446 | $ docker run -v /home:/test ... 447 | ``` 448 | 449 | 在第一种情况下,由于你并没有显示声明宿主机目录,那么 Docker 就会默认在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。而在第二种情况下,Docker 就直接把宿主机的 /home 目录挂载到容器的 /test 目录上。 450 | 451 | 452 | 453 | 镜像的各个层,保存在 /var/lib/docker/aufs/diff 目录下,在容器进程启动后,它们会被联合挂载在 /var/lib/docker/aufs/mnt/ 目录中,这样容器所需的 rootfs 就准备好了。 454 | 455 | 456 | 457 | 容器会在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。 458 | 459 | 460 | 461 | 由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。从而保证了容器的隔离性不会被 Volume 打破。 462 | 463 | 464 | 465 | 这里的挂载技术就是Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。 466 | 467 | ![img](Docker容器实现原理/95c957b3c2813bb70eb784b8d1daedc6.png) 468 | 469 | mount --bind /home /test,会将 /home 挂载到 /test 上。其实相当于将 /test 的 dentry,重定向到了 /home 的 inode。这样当我们修改 /test 目录时,实际修改的是 /home 目录的 inode。 470 | 471 | 472 | 473 | ## Reference 474 | 475 | * [Docker overview](https://docs.docker.com/get-started/overview/) 476 | 477 | * [DOCKER基础技术:LINUX NAMESPACE](https://coolshell.cn/articles/17010.html) 478 | 479 | * [DOCKER基础技术:AUFS](https://coolshell.cn/articles/17061.html) 480 | 481 | * [DOCKER基础技术:LINUX CGROUP](https://coolshell.cn/articles/17049.html) 482 | 483 | * [白话容器基础(二):隔离与限制](https://time.geekbang.org/column/article/14653) 484 | 485 | * [白话容器基础(三):深入理解容器镜像](https://time.geekbang.org/column/article/17921) 486 | 487 | * [白话容器基础(四):重新认识Docker容器](https://time.geekbang.org/column/article/18119) 488 | 489 | 490 | 491 | -------------------------------------------------------------------------------- /深入k8s/Docker容器实现原理/2017-11-30-docker-filesystems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/Docker容器实现原理/2017-11-30-docker-filesystems.png -------------------------------------------------------------------------------- /深入k8s/Docker容器实现原理/8a7b5cfabaab2d877a1d4566961edd5f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/Docker容器实现原理/8a7b5cfabaab2d877a1d4566961edd5f.png -------------------------------------------------------------------------------- /深入k8s/Docker容器实现原理/95c957b3c2813bb70eb784b8d1daedc6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/Docker容器实现原理/95c957b3c2813bb70eb784b8d1daedc6.png -------------------------------------------------------------------------------- /深入k8s/Pod对象/8c016391b4b17923f38547c498e434cf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/Pod对象/8c016391b4b17923f38547c498e434cf.png -------------------------------------------------------------------------------- /深入k8s/Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/Readme.md -------------------------------------------------------------------------------- /深入k8s/在k8s中运行第一个程序/k8s集群图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luozhiyun993/luozhiyun-SourceLearn/1988f4d24aa76603381ea24dfed143105889d685/深入k8s/在k8s中运行第一个程序/k8s集群图.png --------------------------------------------------------------------------------