Go 中 for range 和 slice 的坑

for range 的实现 下面这段代码会永无止境的循环吗 ? package main import ( "fmt" ) func main() { sl := []int{1,2,3,4} for _, v := range sl{ sl = append(sl, v) } fmt.Println(sl) } 要验证它很简单,运行一下即可得到结果,最后的结果是 [1 2 3 4 1 2 3 4] 要理解为什么会有这样的结果不难,首先我们需要清楚一点 go 语言中的赋值语句都是赋值,那么就意味着 如果赋值的是一个指针, 那么拷贝的是指针指向对象的地址(就是一个数值, 至于这个数值有多大, 具体要看运行的平台)也就是指针的值 如果赋值的是一个对象, 那么就会拷贝这个对象 然后,我们再来看一下,当 for range 遇到不同的迭代对象时,编译器是如何展开代码的 数组 range_temp := range len_temp := len(range) for index_temp = 0; index_temp < len_temp; index_temp++ { value_temp = range_temp[index_temp] index = index_temp value = value_temp original body } slice 切片 for_temp := range len_temp := len(for_temp) for index_temp = 0; index_temp < len_temp; index_temp++ { value_temp = for_temp[index_temp] index = index_temp value = value_temp original body } map // Lower a for range over a map. // The loop we generate: var hiter map_iteration_struct for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) { index_temp = *hiter.key value_temp = *hiter.val index = index_temp value = value_temp original body } 注: ...

2022-07-29 · Me

一致性哈希 Consistent Hashing

在理解一致性 hash 之前,先来看看这个问题是怎么产生的。 例如我们有一个数据库,里面存了成千上万张图片,用户访问图片会呈现一定的规律性,比如某段时间内一张照片火了,那么短时间内会有大量的请求访问这张图片,会对数据库造成压力,这时候我们就想到要加一层缓存,这也是系统架构的终极法宝。 所以,这种场景下典型的系统架构如下图所示: 客户端要访问文件名为 A 的图片,代理服务器根据文件名去缓存服务器中查询,如果cache中没有,那么最终去数据库取,同时也把取到的图片文件缓存在某个缓存服务器中。 这里有个题外话,通常我们会把文件名 A 通过 hash 函数转换成一段数字,便于操作,这里的 hash 函数与本文的一致性 hash 是不一样的。 那么问题来了:如何将图片均匀的缓存在缓存服务器上呢? 最简单的方式,以文件名为 key,缓存服务器个数为 N,取模得到余数,即 key % N = i,i 是几,就把图片缓存到对应编号的服务器上。 这种方式确实能够将数据 均匀的 分布在缓存上,但是最大的缺点是一旦 N 的数量发生变化,那么几乎所有的 i 都会改变,导致缓存失效。 例如, key = 5 的文件,在 N = 3 时,缓存在编号为 2 的缓存服务器上。 增加一台服务器,N = 4,那么 key = 5 的文件应该在 1 号服务器上,但事实上它在 2 号。 导致这种情况的根本原因是什么呢? 我们想让数据均匀分布,但是均匀的算法却依赖于 N ,而 N 直接依赖于服务器的数量! 一致性 Hash 的原理 消除依赖 所以解决的办法就是,让均匀分布的计算方法不依赖于 Redis 的个数 N。 ...

2022-07-09 · Me

etcd-raft 源码阅读之 raftLog

etcd-raft 有关 log 的实现在分布在log.go,log_unstable.go,storage.go 三个文件中。首先看一下 raftLog 结构体。 raftLog 结构体 type raftLog struct { // storage contains all stable entries since the last snapshot. storage Storage // unstable contains all unstable entries and snapshot. // they will be saved into storage. unstable unstable // committed is the highest log position that is known to be in // stable storage on a quorum of nodes. committed uint64 // applied is the highest log position that the application has // been instructed to apply to its state machine. // Invariant: applied <= committed applied uint64 } 其中 Storage 存放 stable 的 log,它是一个接口,具体实现可由应用层控制,在 raftexample 和 etcd server 中都是用了 默认的实现 MemoryStorage unstable 存放的是还未放到 stable 中的 log,可见实际上无论是 stable 还是 unstable,他们都是存在内存中的,那么不怕断点导致的丢失吗? 其实真正生产环境中使用的 etcd server 在写入 MemoryStorage 前还要写入 WAL 和 snapshot,也就是说,etcd的稳定存储是通过快照、预写日志、MemoryStorage 三者共同实现的。具体细节本文先不讨论。 committed 表示该节点所知数量达到quorum的节点保存到了 stable 中的日志里,index最高的日志的index applied 表示该节点的应用程序已应用到其状态机的日志里,index最高的日志的index。 由此可见,committed 和 applied 都是在 stable 中,不在 unstable。 他们的关系如下所示 ...

2022-05-22 · Me

etcd-raft 源码阅读之 Leader 的选举

首先看一下 raft node 之间传递的基本消息(比如 leader 选举,AppendLog)类型 Message protobuf 定义 message Message { optional MessageType type = 1 ; optional uint64 to = 2 ; optional uint64 from = 3 ; // 整个消息发出去时,所处的任期 optional uint64 term = 4 ; // logTerm is generally used for appending Raft logs to followers. For example, // (type=MsgApp,index=100,logTerm=5) means leader appends entries starting at // index=101, and the term of entry at index 100 is 5. // (type=MsgAppResp,reject=true,index=100,logTerm=5) means follower rejects some // entries from its leader as it already has an entry with term 5 at index 100. // 该消息携带的第一条Entry记录的的Term值 optional uint64 logTerm = 5 ; // 索引值,该索引值和消息的类型有关,不同的消息类型代表的含义不同 optional uint64 index = 6 ; repeated Entry entries = 7 ; // 已经提交的日志的索引值,用来向别人同步日志的提交信息。 optional uint64 commit = 8 ; // 在传输快照时,该字段保存了快照数据 optional Snapshot snapshot = 9 ; optional bool reject = 10; optional uint64 rejectHint = 11; optional bytes context = 12; } message Entry { optional uint64 Term = 2; // Index:当前这个entry在整个raft日志中的位置索引, // 有了Term和Index之后,一个`log entry`就能被唯一标识。 optional uint64 Index = 3; optional EntryType Type = 1; optional bytes Data = 4; } raftexample 目录提供了一个如何使用 raft library 的例子, 首先从 main.go 来看如何启动一个 raft node。 ...

2022-05-15 · Me

记一次 go-redis 的 debug 过程

Background 本文背景是这样的: 有一个线上服务使用了 go-redis 库连接 redis,目前 QPS 也不是很高,大约每秒一次的样子,但是通过 log 发现每次 redis 操作花费的时间如下: redis call cost: 0 ms redis call cost: 2 ms redis call cost: 0 ms redis call cost: 1 ms redis call cost: 0 ms redis call cost: 17 ms redis call cost: 0 ms .... 正常一个简单的 redis get 操作耗费 0-3ms 时间可以理解,但是为什么会出现 17 ms 呢? 而且出现的频率还不低,大概每 30 个正常的中会出现一个。 尝试 debug 首先总结一下场景和条件 service 部署在 k8s 中,大概 10 个 pod 在运行。 整个 service 的 QPS 大概一秒一个,很低。 高延迟的情况大概每 30 个 log 出现一个。 service 使用简单的 redis get(),没有复杂操作。 但是 service 本身是有很多 go routine 并发的。 所以可能出现问题的地方 ...

2022-05-01 · Me

使用 Kubernetes 遇到的一些问题和解决思路

update on 2022-05-21 今天在 homelab 的 k8s 集群上发生了同样的情况,我想删除一个 namespace,再确认已经把 namespace 里面所有其他资源都删除的情况下,namespace 始终是 Terminating, 找了很多资料,方法也众说纷纭 。 最后通过看 api-server log 发现原来又是 Unable to authenticate the request due to an error: x509: certificate has expired or is not yet valid root cause 还是我更新 cert 的时候又漏了某些步骤。 事情的起因是 k8s 的 cert 过期了,在目录 /etc/kubernetes/pki/ 下面的这些 cert 都与 k8s 的核心服务息息相关,因此 cert 过期了,整个 k8s 集群就停止服务了。 这个集群是 kubernetes 1.14, 因此需要运行几个命令完成更新,而 1.15 版本以上这个过程简化了不少。 由于之前已经 renew cert 两次了,因此正常按部就班几个操作就完事了,但是这个因为一点小疏忽,加上系统死机重启了一次,花了很多时间去恢复各种服务。 本文记录 debug 的过程中遇到的一些症状,以及后来发现的解决方法,为以后遇到类似问题提供思路。 Node 重启后 kubelet 没运行 前面提到,可能是因为 cert 过期后触发某些 bug 导致 Master Node 不能 ssh(之前 renew cert 没有类似问题),所以只能去机房按电源开关重启了。 ...

2022-04-27 · Me

gRPC client 如何实现 TCP 重连

之前写过一篇 gRPC-go 建立 TCP 连接的过程 博客,主要研究了 client 程序启动后,如何与 server 建立 TCP 连接。 今天,在思考 redis-go 的连接池实现的时候,突然想到: 当 gRPC 的 TCP 连接断开后,能自动重连吗? 如果可以,是如何实现的 ? 首先要注意,这里指的是 TCP 连接,而不是 http2 中的 stream。 我们知道,gRPC 数据的传输使用 http2 的多路复用,也就是在一个 TCP 连接上有多个全双工的 http2 stream,这里的 stream 如果被断开后怎么重连与 http2 的实现有关,不在本文讨论范围。 对于上面第一个问题,使用 gRPC 的经验告诉我是可以自动重连的,不妨再做个简单的测试,client 端代码如下: func main() { conn, _ := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure()) defer conn.Close() cli := protobuf.NewTestClient(conn) req := &protobuf.EchoRequest{ Msg: "hi", } for i := 0; i < 10000; i++ { time.Sleep(time.Second) resp, err := cli.Echo(context.Background(), req) if err != nil { log.Printf("%v\n", err) continue } log.Printf("[D] resp: %s", resp.Reply) } } server 端代码略。 启动 client 后,不断启动和 ctrl-c 结束 server,证实 client 能自动重连 TCP 。 使用 netstat 查看 TCP 连接也能看到 client 使用了新的端口号重连。 ...

2022-04-04 · Me

使用 nsenter 从 k8s Pod 逃逸到 Host

本文是读完 Detecting a Container Escape with Cilium and eBPF 和 使用 Cilium 增强 Kubernetes 网络安全 的一个简单总结。 如何从 Pod 逃逸到 Host 通常为了安全起见,生产环境的 docker image 都要求不使用 root,一般都是在 Dockerfile 中指定 USER xxx,这样启动的 container/pod 是使用非特权的 user,这样的 user 是没法用 sudo 安装软件的。 有时为了能临时 debug,需要安装 vim, curl 之类的命令,又不想改动 Dockerfile 重新 build image,该怎么办呢? 一个 k8s 原生支持的方法是在 deployment 里面指定 securityContext,如下所示 $ cat privileged.yaml apiVersion: v1 kind: Pod metadata: name: privileged-the-pod spec: hostPID: true hostNetwork: true containers: - name: privileged-the-pod image: nginx:latest ports: - containerPort: 80 securityContext: privileged: true 对于 docker container,可以在指定 docker run 命令时,设置 --user 为 0 也能获得 root 的 container。 ...

2022-04-03 · Me

Rust WASM 自制俄罗斯方块游戏

起因 事情的起因是在 Switch 上玩 Tetris99 游戏,由于不喜欢这种吃鸡的形式,只想玩小时候的那种掌机模式,于是想到可不可以自己做一个。 有了这个想法以后,打算使用 Rust + WASM,一方面是学习一下新技术,另一方面考虑到能直接在浏览器运行,可以跨平台,甚至可以在电视机上用浏览器打开网页就可以玩。 选定技术栈以后,在 Github 上搜了一下,发现早有人做了类似的工作,不过没关系,主要还是要自己实现一下。 几种技术方案 学习了一圈以后,理解了用 Rust + WASM 实现一个 web 游戏的大体思路。 首先,Rust 的 wasm-bindgen 库必不可少,这是连接 rust 代码和 wasm 之间的桥梁。 其次,既然是 web 游戏,那么免不了要画图,如何画图呢? 大家都不约而同的选择了 HTML 的 canvas,这是一种 html 标准自带的画图方式,比如用下面这样简单的代码,就能画一个矩形。 <html> <body> <canvas id="myCanvas" width="200" height="100" style="border:1px solid #000000;"> </canvas> </body> </html> 所以,本质上我要做的就是用 Rust/WASM 代码 或者 JavaScript 代码,控制这个 <canvas id="myCanvas" ,并且定期刷新,这样就能显示动画效果了。 如果你是个 JavaScript 高手,并且打算全部用 JavaScript 实现,那么现在就可以开始动手了。 但如果是 Rust WASM 的方式,还需要考虑下是 纯 WASM 实现呢? 还是 WASM 实现核心算法逻辑,JavaScript 实现画图这样的组合方式? ...

2022-03-06 · Me

Rust 异步 async 和 tokio

快速入门 Rust 语言原生提供了异步操作的关键字 async 和 await,但通常还需要配合第三方的 runtime,其中最有名的就是 tokio 了。 在开始了解 Rust 的所谓异步是什么样子之前,先看一下如何写一个简单的 Rust 异步程序。 以下是 main.rs async fn hello_world() { hello_cat().await; println!("hello, world!"); } async fn hello_cat() { println!("hello, kitty!"); } #[tokio::main] async fn main() { let future = hello_world(); println!("start"); future.await; } Cargo.toml 文件中加入一行 [dependencies] tokio = { version = "1", features = ["full"] } 运行上面的代码,会看到这样的输出 start hello, kitty! hello, world! 可以看出,future = hello_world(); 是创建一个异步执行的代码块, 并把它赋值给了 future 变量,这个代码块不会立刻执行,而是等到用户调用 await 的时候再去执行。 ...

2022-02-27 · Me