Deadlock检测时机

Go语言的运行时会在所有活跃的goroutine都无法继续执行时判定为死锁。这意味着,只要还有至少一个goroutine能够正常运行,Go运行时就不会触发死锁检测机制。死锁检测通常在以下情况下触发: 所有goroutine都阻塞:如果所有的goroutine都在等待某些事件(如通道操作、锁获取等),而这些事件无法由其他goroutine触发(因为没有其他goroutine在运行或者能够解除阻塞状态),Go运行时就会判定程序为死锁状态。(这里不包括io等待,因为io等待是阻塞的,但是go的运行时并不会触发死锁检测机制) 这种自动死锁检测主要是为了帮助开发者在开发阶段识别出潜在的并发问题。然而,它的能力是有限的,特别是在涉及网络I/O、系统调用或者复杂锁逻辑的情况下,Go的死锁检测可能不会触发。因此,即使Go运行时没有报告死锁,也不代表程序中不存在潜在的并发问题。 为了避免死锁,推荐的做法包括: 避免循环等待:设计系统时应确保资源的分配顺序一致,以避免循环等待的情况发生。 使用适当的同步原语:比如使用带缓冲的channel、正确使用锁(如sync.Mutex)、以及其他并发控制工具(如sync.WaitGroup、context.Context等)。 限制并发数:有时通过限制系统中并发执行的goroutine数量可以简化资源管理,减少死锁的风险。 彻底测试:并发程序应该经过详尽的测试,包括使用竞态检测工具(如Go的-race标志)来帮助识别并发错误。 这些措施可以帮助开发者构建更健壮、更可靠的并发程序。

<span title='2024-05-08 17:52:55 +0800 +0800'>五月 8, 2024</span>

mysql中time_zone的作用,以及为什么要配置本地时区和db时区

mysql中time_zone的作用,以及为什么要配置本地时区和db时区 1. mysql中time_zone的作用 mysql中time_zone的作用,就是用来设置时区的(废话),time_zone影响TIMESTAMP类型和NOW等函数的值,但并不影响DATETIME类型和DATE类型。 因为TIMESTAMP存储的时时间戳,当用户发送过来一个时间如“2023-12-11 10:41:18”,那么mysql会将其时间戳,这个时间就是按time_zone设置的时区来解析的,然后转换成时间戳保存, 在给用户查询时在通过时区转换会时间串。 DATETIME类型不是时间戳,存入时是“2023-12-11 10:41:18”,查询时也是“2023-12-11 10:41:18”,不会做转换,也和时区无关 2. 为什么要配置本地时区和db时区 本地时区是给mysql驱动使用,并不会影响mysql的时区,只会影响mysql驱动的解析, db时区是mysql的时区,会影响mysql的时区,会影响mysql的查询结果。 本地时区作用 在读取到mysql发来的时间,go会按照本地时区来解析,转换为time.Time类型mysql按照数据库time_zone返回时间串后,go并不知道用哪个时区来解析这个时间串,所以需要设置本地时区。 本在写入time.Time类型时,go会把time.Time转换为本地时区发送给mysql 比如设置本地时区为“Asia/Shanghai” root:root@tcp(127.0.0.1:3306)/db?charset=utf8&loc=Asia%2FShanghai&parseTime=true通过loc参数来这设置 同时设置本地和数据库时区 root:root@tcp(127.0.0.1:3306)/db?charset=utf8&loc=Asia%2FShanghai&parseTime=true&time_zone=%27%2B8%3A00%27 dsn上的参数有的是给数据库驱动使用,有的是给mysql使用,具体可参阅mysql驱动的DSN

<span title='2023-12-11 10:41:18 +0800 +0800'>十二月 11, 2023</span>

Viper读取自定义远程配置

实现viper.remoteConfigFactory接口 type remoteConfigProvider struct{} //这个接口是在第一次读取远程配置时调用,viper.ReadRemoteConfig() func (rc remoteConfigProvider) Get(rp viper.RemoteProvider) (io.Reader, error) { log.Println("Getting config from remote", rp) r := bytes.NewReader([]byte("{\"a\":1}")) time.Sleep(time.Second) return r, nil } //这个接口是在客户端调用viper.WatchRemoteConfig(), 只监听一次变化 func (rc remoteConfigProvider) Watch(rp viper.RemoteProvider) (io.Reader, error) { //这里写你获取配置的方法 log.Println("Watching config from remote", rp) s := fmt.Sprintf(`{"app":{"name":"test","version":%d}}`, time.Now().Unix()) r := bytes.NewReader([]byte(s)) time.Sleep(time.Second) return r, nil } //这个接口是在客户端调用viper.GetViper().WatchRemoteConfigOnChannel()时,监听多次变化 func (rc remoteConfigProvider) WatchChannel(rp viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) { log.Println("Watching Channel config from remote", rp) ch := make(chan *viper.RemoteResponse) go func() { //defer close(ch) viper里面使用死循环来处理,如果关闭管道会返回nil, 而viper没有处理,会报painc,所以不用关闭,viper是想让你在整个生命周期都监听 //这里写你获取配置的方法 for i := 0; ; i++ { ch <- &viper.RemoteResponse{ Value: []byte(fmt.Sprintf(`{"app":{"name":"test","version":%d}}`, time.Now().Unix())), } time.Sleep(time.Second) } }() return ch, nil } 设置viper必要参数 func init() { viper.RemoteConfig = remoteConfigProvider{} //把我们写的provider设置好,否则监听不会生效 viper.SupportedRemoteProviders = []string{"app"} //这里名称需要设置好,和下面添加的名称需要一致 } 使用viper viper.SetConfigType("json") //需要和返回的数据格式一致,不然无法解析 viper.AddRemoteProvider("app", ":8080", "/aabb/cc") //app就是上面的SupportedRemoteProviders,viper内部会校验是否包含,后面两个参数根据自己的需求来填写 viper.ReadRemoteConfig() //加载远程配置 // viper.WatchRemoteConfig() //一次性监听配置,阻塞 viper.GetViper().WatchRemoteConfigOnChannel() //一直监听,非阻塞

<span title='2023-09-15 15:56:44 +0800 +0800'>九月 15, 2023</span>

在docker中go编译应注意的问题

1.使用golang:tag 这种镜像编译后在alpine中会报错(not found) 原因是:go默认会使用glibc,而alpine中没有glibc,所以会报错 解决办法有三种: 连接RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86_64.so.2,建立一个glic链接 使用golang:tag-alpine镜像编译,编译系统和运行系统都用alpine 编译时禁用CGO CGO_ENABLED=0 go build编译出来的文件不依赖动态库

<span title='2023-09-07 11:17:42 +0800 +0800'>九月 7, 2023</span>

内存泄漏

因一次线上内存泄漏问题的总结 go 内存泄漏大概有一下几种情况 长期存活的对象:如果某些对象的生命周期较长,但在使用完之后没有被正确释放,这些对象将一直保留在内存中,导致内存泄漏。 协程泄漏:在 Go 中,每个协程(goroutine)都是轻量级的并发执行单元。如果你启动了太多的协程并且没有适时关闭或终止它们,那么这些协程就会一直存在并占用内存,导致内存泄漏。 循环引用:如果存在对象之间的循环引用关系,即两个或多个对象相互引用,而没有其他的引用指向它们,这将阻止垃圾回收器回收这些对象,导致内存泄漏。 意外的全局变量引用:如果你在函数中意外保留了对全局变量的引用,即使函数执行完毕,这个函数所涉及的内存也无法释放,导致内存泄漏。 未关闭的文件或网络连接:如果在读写文件、网络通信或数据库连接等操作后,忘记关闭这些资源,将会导致系统资源泄漏,最终影响内存的使用。 不正确的缓存管理:当使用缓存来存储数据时,如果没有合适地管理缓存的大小和生命周期,可能会导致过多的对象一直存在于缓存中而无法被释放,从而引发内存泄漏。 未正确使用释放资源的函数:在使用第三方库或 API 时,如果没有按照正确的方式使用和释放资源,比如数据库连接或操作系统句柄等,将导致资源泄漏,可能会间接导致内存泄漏。 这次出问题的是未关闭网络连接(集群内其他服务)没有关闭http Response.Body,导致这两个程序内存一直增长,一个服务内存泄漏导致另一个服务goroutine无法释放,到达k8s内存上限被kill掉 排查方法 通过代码pprof抓取相应数据 导入net/http/pprof自动注册内置好的处理方法,开启默认http服务 我们主要看第二种,第一种可以自己看net/http/pprof里面的写法或者参考文档 用浏览器查看http[s]://xxx/debug/pprof页面,一共有如下几个指标 allocs: 内存过往统计,可以看运行过程中哪些过程使用内存最多 block: 看有哪些被阻塞了,比如各种io cmdline: 运行程序时的名称和参数 比如`./app a b c` goroutine: goroutine信息 heap: 在用堆内存信息 mutex: 锁信息 profile: CPU信息 threadcreate: 系统线程创建信息 trace: 追踪信息,比如GC, 各个goroutine调度等 其中allocs/block/goroutine/heap/mutex/profile 都可以使用go tool pprof -http=":8080" http[s]://host:port/debug/pprof/xxxxx(其中xxxxx用前面几种指标代替) 查看详细信息 trace需要先点击trace下载,然后使用go tool trace tracefile来查看

<span title='2023-08-03 09:57:26 +0800 +0800'>八月 3, 2023</span>

go 编译指令build

go:build 是 Go 1.17 版本中引入的一个构建标记,用于根据不同的条件控制代码的编译。go:build 格式如//go:build <标记表达式>, 1.17之前使用+build的方式,这里我们介绍更强大的的go:build. 表达式可以包含由 ||、&& 和 ! 运算符和括号组合的选项,含义与 Go 语言中相同. 构建约束可以出现在任何类型的源文件中(不仅仅是 Go 文件),但是它们必须出现在文件的顶部附近,仅由空行和其他行注释分隔。这些规则意味着在 Go 文件中,构建约束必须出现在包声明之前。 例如,下面的构建约束将约束一个文件在满足“linux”和“386”约束条件或在满足“darwin”条件且未满足“cgo”条件时进行构建://go:build (linux && 386) || (darwin && !cgo) 自定义tag //go:build atest 当我们使用go build -tags atest的情况下会编译此文件,否则不会编译 控制go版本 //go:build go1.17 指定当前文件>=go1.17才编译 //go:build go1.17 && go1.20 版本>= go1.17 并且<= go1.20

<span title='2023-03-07 10:50:47 +0800 +0800'>三月 7, 2023</span>

go 编译指令Linkname

go:linkename是go的编译指令,可以在一个包中使用另一个包中的非导出函数或变量 具体使用方法举例 在pkg1包中有个未导出的函数add package pkg1 func add(a, b int) int { return a + b } 在pkg2中想要使用pkg1.add,正常来说add开头是小写是不能被另一个包使用的,这时候我们想要在另一个包中使用就有两种方法: 在pkg1中定义一个可以导出的函数,这种就是在写个Add函数包裹一下,这种就不举例了 在pkg2中使用linkname编译指令修改函数或变量的名称和可见性(不太建议) 这里我们举例第二种 package pkg2 import ( _ "goasm/pkg1" //需要引入pkg1, 让pkg1参与代码编译,否则编译器找不到, 这句也可以写在别的文件中,只要让pkg1参与编译就行, relocation target goasm/pkg1.add not defined _ "unsafe" //编译器要求导入unsafe包,否则不能使用linkname指令 ) //下面就是linkname编译指令使用方法, 在函数声明上添加注释, 其中pkg1_add是函数名称, goasm/pkg1.add 是要连接的函数, 这句指令的效果就是,调用pkg1_add就是在调用goasm/pkg1包中的add函数 //go:linkname pkg1_add goasm/pkg1.add func pkg1_add(a, b int) int func Add2(a, b int) int { return pkg1_add(a, b) } 总结: go:linkname的使用是依赖于编译器的实现,因此使用时需要慎重考虑其可维护性和可移植性。 总之了解就好,不是迫不得已不要真的使用

<span title='2023-02-24 14:35:25 +0800 +0800'>二月 24, 2023</span>

数据竞赛(Data Race)

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

<span title='2022-06-23 16:39:21 +0800 +0800'>六月 23, 2022</span>

go内存模型

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

<span title='2022-06-23 10:33:29 +0800 +0800'>六月 23, 2022</span>