DDIA 第九章 一致性和共识

一致性保证 本章主要包含了以下话题: 常用的强一致性模型:线性一致性,的优点和缺点。 分布式系统中的事件顺序,特别是因果关系和全局顺序的问题。 如何原子的提交分布式事务,也就是说如何解决共识问题。 线性一致性 哪些地方依赖于线性一致性呢 ? Locking 服务和 leader election,比如加锁出错了会导致两个人同时写同一个文件。 账户余额,产品库存信息,比如产品超卖。 如何实现一个线性一致性系统 在分布式系统里面,产生不一致的根本原因是因为数据有多个副本,而更新这些副本不是原子操作。 以下是几种多副本系统,能否实现线性一致性的比较: Single-leader replication (可能线性一致) 这里我猜作者说的是传统 MySQL 这样的主从复制技术。 Consensus Algorithms (线性一致) 这里应该就是常说的 Panox 和 Raft 了。 Multi-leader replication (非线性一致) Leaderless replication (也许不是线性一致的) Dynamo 风格 和 Cassandra 风格。 线性一致的代价 假设两个 datacenter 之间网络断了的情况下, 如果是 multi-leader 系统,那么每个 datacenter 仍然可以独立运行,datacenter 之间的数据是异步同步的,所以不会受到影响。 如果是 single-leader 系统,如果 client 连到了全是 follower 的 datacenter,那么所有 write 和 linerizable read 都受影响,如果 client 连到的是 leader 所在的 datacenter,则不受影响。 CAP Consistency, Availability, Partition Tolerance. ...

2022-02-20 · Me

Makefile 的几个语法坑

Makefile 和 Bash script 在使用的过程中有很多奇奇怪怪的坑,本文做一下纪录。 首先,有两个文件,一个叫 envs,里面定义了一个环境变量,比如 $ cat envs export GOPROXY="test.local" 第二个文件就是 Makefile ,假如我这样写 test: source ./envs echo ${GOPROXY} 所以,总的目标是,我希望在 Makefile 中导入另一个文件中事先定义好的环境变量。 然而这样的写法有很多问题。 source 命令找不到 加入直接运行 make, 很有可能你会看到这样的错误 $ make source ./envs make: source: Command not found 可是在 terminal 里面明明可以用 source 命令啊? 于是,第一个坑出现: source is a (non-POSIX) shell builtin, not an executable program on any conventional UNIX-like system. If source is changed to ., make changes its strategy; instead of trying to find and execute a program, it just passes the rule body to the system shell. ...

2021-09-26 · Me

Gin HTTP 框架学习笔记

最近要做一个 REST API server,在网上搜索了一遍以后,发现常用的是 Gin 和 Echo,并且很多人都说 golang 本身提供的 http server 已经足够强大,gin 和 echo 也只是在外包了一层。 我看 Gin 的源码行数比 Echo 少很多,而且测试覆盖率也高很多,因此决定学习一下 Gin,本文目标有以下这些 学习如何设计一个 REST 风格的 server ? 学习 Gin 在 go 自带的 http server 基础上做了哪些工作? 启动 Gin http server 在使用 Gin 框架的时候,最后都会调用 gin.Run(":8080") ,这样你的 http server 就可以就收 client 请求了, func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) return } 可见,Run 函数最后调用了 http.ListenAndServe,所以说 http 协议层的解析等工作都是 go 标准库完成的,Gin 只负责后续针对不同 URL 的路由 (Router) 工作。 ...

2021-09-03 · Me

错误使用 time.After() 导致内存泄漏

今天看到了一篇有关 timer 泄露的文章,觉得很有意思,于是把它记录下来。 一般没有问题的写法 说道 time.After() 会导致内存泄露,很多人一定会觉得奇怪,因为代码里经常会用到它,也没见有内存泄漏啊? 是的,一般我们这样写的话是没有问题的 func main() { ch := make(chan int) go func() { ch <- 1 }() select { case _ = <-ch: case <-time.After(time.Second * 1): fmt.Println("timeout") } } 有问题的写法 那么,什么样的写法有问题呢? 当使用 for loop 的时候,比如这样 for { select { case _ = <-ch: // do something... continue case <-time.After(300 * time.Millisecond): fmt.Printf("time.After() fire!\n") } } 很不幸的是,上面这样的写法也非常常见,我自己就写过这样的代码。那么它真的会造成内存泄露吗?试一下便知道 前一篇博客中已经介绍了如何使用 pprof 对 Go 程序进行 profiling,简单提一下步骤 在代码中引入 _ "net/http/pprof", 并开启一个http server 导出 metrics 运行你的 binary 执行 go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap 浏览器就会自动打开 localhost:8081 显示结果了 测试代码如下: package main import ( "fmt" "net/http" _ "net/http/pprof" "time" ) func main() { go func() { ip := "127.0.0.1:6060" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }() ch := make(chan int, 10) go func() { in := 1 for { in++ ch <- in } }() for { select { case _ = <-ch: // do something... continue case <-time.After(3 * time.Minute): fmt.Printf("time.After() fire!\n") } } } 使用 pprof 后,可见 timer 内存占用居然超过了 1GB !! ...

2021-08-08 · Me

k8s 切换 namespace 以及命令补全

本文可以学到 .kube/config 文件中有哪些内容 如何实现 bash 的命令补全功能 起因 在使用 kubectl 命令的过程中,经常需要查看不同 namespace 下的资源,因此命令经常需要带上 -n name。 如果不想每次都多打这些字符,也可以设置一个默认的 namespace, kubectl config set-context --current --namespace=xxxx 这样是方便了不少,但是一旦切换了 namespace 之后,又要重复上面的命令,而且经常还不记得。 有没有更好的办法呢? 有人开发了一个小工具,kubectx 专门用于方便的切换 ctx 和 namespace。 ctx 是什么呢? 其实就是哪个 k8s 集群。 说白了就是让你方便的在多个集群和 namespace 之间切换。 kubectx 有两种实现,一开始用的是最简单的 bash shell 脚本,新的版本开始用 k8s client API 开发。 下文的分析仅仅关注 namespace 的切换。 shell 版本的实现 这个实现非常简单,本质上就是调用几个 kubectl 命令实现 ns 切换。 首先需要知道的是,在 ~/.kube/config 路径下的 config 记录了你配置 kubectl 的信息,比如你用 kubectl 操作过几个 k8s 都会纪录在里面。 apiVersion: v1 clusters: - cluster: certificate-authority-data: DATA+OMITTED server: https://10.180.117.162 name: gke_hyrule-dev_us-central1_hyrule-us-central1 contexts: - context: cluster: gke_hyrule_us-central1 namespace: mi-hyrule user: gke_hyrule-dev_us-central1_hyrule-us-central1 name: gke_hyrule-dev_us-central1_hyrule-us-central1 - context: cluster: user: name: current-context: gke_hyrule_us-central1 kind: Config preferences: {} users: - name: gke_hyrule-dev_us-central1_hyrule-us-central1 user: auth-provider: config: access-token: ya29.c.Ko8BCghf9Cp47iz7usZH24k_ask9OD6E4KEh-Z cmd-args: config config-helper --format=json cmd-path: /usr/lib/google-cloud-sdk/bin/gcloud name: gcp 如果是多个 k8s 的话将会有多个 contexts 和 users 字段。 ...

2021-08-01 · Me

Golang pprof 的使用姿势

首先,在代码中引入 pprof 的方式非常简单,只要把下面这段代码放到 main 函数中即可 _ "net/http/pprof" go func() { if err := http.ListenAndServe(":9090", nil); err != nil { panic(err) } os.Exit(0) }() 然后启动你的程序,再用以下这些命令去对应的端口做 profiling // cpu profile 默认从当前开始收集 30s 的 cpu 使用情况,需要等待 30s go tool pprof http://47.93.238.9:9090/debug/pprof/profile # wait 120s go tool pprof http://47.93.238.9:9090/debug/pprof/profile?seconds=120 // 以下 second 参数不起作用,因为采样是一瞬间完成的 go tool pprof http://47.93.238.9:9090/debug/pprof/heap go tool pprof http://47.93.238.9:9090/debug/pprof/goroutine go tool pprof http://47.93.238.9:9090/debug/pprof/block go tool pprof http://47.93.238.9:9090/debug/pprof/mutex 还有一种是 import "runtime/pprof“的方式,这种不太常用,不在本文范围。 运行了 go tool pprof 命令以后,会进入到一个交互界面, $ go tool pprof "http://localhost:9090/debug/pprof/goroutine" Fetching profile over HTTP from http://localhost:9090/debug/pprof/goroutine //出现以下内容,代表采样已经完成,可以查看了 Saved profile in /Users/xx/pprof/pprof.goroutine.001.pb.gz File: xx Type: goroutine Time: Jul 18, 2021 at 4:55pm (PDT) Entering interactive mode (type "help" for commands, "o" for options) (pprof) quit 一旦进入了可交互模式后,代表采样已经完成,这时候可以 输入 svg > xxx.svg可以生成一个图片存下来,或者 输入 quit 退出,所有数据文件存在上面提到的目录下。 然后就是查看这个 perf data 了,用 go tool pprof data.pb.gz可以进入到一个命令行界面 ...

2021-07-18 · Me

Rust 入坑之 LRU Cache

lru 算法的原理简而言之就是一个 hash ,一个 double linked list Linked List 提供 O(1) 的复杂度对元素进行插入和删除 hash 提供 O(1) 的复杂度进行查找 本文主要是通过阅读一个 rust 实现的 lru 学习相关语法。 如何在结构体里面使用指针? rust 是否有 raw pointer 直接指向内存地址,如果能用该怎么用? Linked List 节点结构体 上面提到,真正的 key/value 是存在双链表的 Node 里,所以需要先定义这个 Node 长什么样,lru-rs 中 LruEntry 表示的就是 node: K V 代表的是泛型的类型, struct LruEntry<K, V> { key: mem::MaybeUninit<K>, val: mem::MaybeUninit<V>, prev: *mut LruEntry<K, V>, next: *mut LruEntry<K, V>, } 下面是如何初始化一个 Node, impl<K, V> LruEntry<K, V> { fn new(key: K, val: V) -> Self { LruEntry { key: mem::MaybeUninit::new(key), val: mem::MaybeUninit::new(val), prev: ptr::null_mut(), next: ptr::null_mut(), } } fn new_sigil() -> Self { LruEntry { key: mem::MaybeUninit::uninit(), val: mem::MaybeUninit::uninit(), prev: ptr::null_mut(), next: ptr::null_mut(), } } } key value 用 mem::MaybeUninit::new(key)进行初始化 prev next 指针用 ptr::null_mut() 初始化 LRU cache 结构体 链表的 node 定义好以后,双链表结构也自然而然就有了。接下来还缺一个 map 结构体,这个可以用 rust 原生的 hash 函数库,然后就可以定义出 LRU 结构体 ...

2021-07-11 · Me

bbolt 的设计与实现

关于 bbolt 的分析,网上已经有很多资料,本文只是对资料和源码的整理,主要是自己的学习笔记,文章最后的参考资料中有更多链接。 bbolt DB 整体组织 首先,bbolt 的一个文件是一个 DB,DB 中可以有多个 table, 每一个 table 是一个 B+ 树。而这个 table 在源码中就是 bucket, 整个 DB 就是一个大 bucket,它的子节点有多个 bucket。整体结构如图所示: 顶层 B+ 树,比较特殊,称为 root bucket,其所有叶子节点保存的都是子 bucket B+ 树根的 page id 其他 B+ 树,不妨称之为 data bucket,其叶子节点可能是正常用户数据,也可能是子 bucket B+ 树根的 page id。 这样,就清楚的知道了 bbolt 中 DB,table,和 data 是如何组织的了。 bbolt 的源码很简洁,主要功能分布在以下几个文件: bucket.go:对 bucket 操作的高层封装。包括 kv 的增删改查、子 bucket 的增删改查以及 B+ 树拆分和合并。 node.go:对 node 所存元素和 node 间关系的相关操作。节点内所存元素的增删、加载和落盘,访问孩子兄弟元素、拆分与合并的详细逻辑。 cursor.go:实现了类似迭代器的功能,可以在 B+ 树上的叶子节点上进行随意游走。 page.go: page 是磁盘上一个 4kb 页的表示,注意,相比 page,第二行提到的 node 表示的是内存里的结构。 db.go : bbolt 的主要源码 tx.go : bbolt 实现 MMVC 的主要代码。 如何加载文件到内存,成为一个DB? 假设有一个数据库文件 data,那么bbolt 的源码是如何读取这个文件,并且在在内存建立DB的呢? ...

2021-07-07 · Me

Bigtable 论文阅读笔记

最近因为工作需要用到 Bigtable,而设计一个好的数据库 Schema 对于性能至关重要,因此想找一些资料看看别人是如何根据自身业务特点设计 schema 的。 在网上找到了一篇 GCP 自己的官方文档 , 里面提到了一些 best practice,也提到了哪些坑需要避免,然而还是看的云里雾里。 比如, Row keys to avoid Row keys that start with a timestamp. This will cause sequential writes to be pushed onto a single node, creating a hotspot. If you put a timestamp in a row key, you need to precede it with a high-cardinality value like a user ID to avoid hotspotting. Row keys that cause related data to not be grouped together. Avoid row keys that cause related data to be stored in non-contiguous row ranges, which are inefficient to read together. ...

2021-06-20 · Me

Rust 入坑之 Bloom Filter

关于 Bloom Filter 的原理不做介绍,网上各种资料满天飞,其中参考资料 1 已经讲解的很详细。 我重点关注如何用 Rust 实现一个简单的 Bloom Filter,并学习一些语法,源码在参考资料 2 。 BloomFilter 结构体 pub struct BloomFilter<T> { hasher: T, k: u32, bit_vec: BitVec, insert_count: u64, } 尖括号中的 T 代表泛型,这样我们就可以使用不同的 hash 函数实现 (hasher) k 表示使用几个 hash 函数,根据 BF 的原理,使用多个 hash 能减少 False Positive bit vec 表示使用一个多大的 bit 数组,这个关系到 BF 的命中率和 FP 率 BitVec 的作用等于是实现了 bloom filter 的 bit array,直接用这个库省略了作者重复实现一个。 定义 BloomHasher 定义这个 trait 的目的是让所有的 hash 函数库都有 hash() 这个函数,方便在上面的 hasher 中调用。 ...

2021-06-19 · Me