利用划分子集限制连接池的大小(1)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 我们的RCP系统中的每个客户端都会针对后端程序程序维持一个长连接发送请求。这些连接通常在客户端启动的时候就建立完成,并且保持活跃状态,不停地有请求通过它们,直到客户端终止。 每个连接都需要双方消耗一定数量的内存和CPU来维护。虽然这个消耗理论上很小,但是一旦数量多起来就可能变得很客观。 上面的两段话来自 在Golang中,每个连接除了接收和发送缓冲区,还会额外对应2个协程。在规模较大的服务中,浪费的资源是非常可观的。 在M个Client,N个Backend的场景中,一共需要建立M * N个连接。这种额外的负担不仅发生在Client端,同样影响会Backend。 一个很朴素的想法是让每个Client只请求所有Backend的一部分。那么划分子集之后,是否会引起其它问题呢? 本文的重点是探讨使用Google给出的子集选择算法二:确定性算法,需要考虑的细节和注意事项。 2. 子集选择算法带来的思考 2.1 目标 当我们引入某个划分子集算法之后,我们担心的问题可能会有以下几个: Backend的负载是否均衡 当Backend集群发生变化(比如有实例宕机,或者进行滚动发布或者重启时,Client的连接池是否有大量的连接需要创建或者销毁 当Backend集群发生变化(比如有实例宕机,或者进行滚动发布或者重启时,Client的请求时的错误率相比没有划分子集时,是否会变大 2.2 子集选择算法 2.2.1 随机选择 先来看看随机算法是否可行, 假定 clientSize=50 backendSize=100 subsetSize=50 生成图片用ggplot2 library(ggplot2) data <- read.table("/tmp/datafile.csv",header=TRUE, sep=",") ggplot(data, aes(X, Y)) + geom_bar(stat = 'identity') subset/random/random.go 显然随机算法的效果不行 2.2.2 确定性算法 func Subset(backends []string, clientID, subsetSize int) []string { subsetCount := len(backends) / subsetSize // Group clients into rounds; each round uses the same shuffled list: round := clientID / subsetCount r := rand.New(rand.NewSource(int64(round))) r.Shuffle(len(backends), func(i, j int) { backends[i], backends[j] = backends[j], backends[i] }) // The subset id corresponding to the current client: subsetID := clientID % subsetCount start := subsetID * subsetSize return backends[start : start+subsetSize] } 只有短短几行,我们来下效果 ...

August 23, 2021 · 1 min

Golang常见mock库小结

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 本文是一篇对mock库的小结。 2. 怎么理解mock这件事 2.1 为什么需要mock 最常见的场景是,一个服务B和它依赖的上游微服务A同时启动开发。为了加快开发速度,服务B不可能等到服务A开发完,在启动开发。因此服务A和服务B约定它们之间交互的接口,然后就可以启动开发了。 服务B为了验证自身的业务逻辑,它需要mock一个A服务(只需要mock相关接口)。但是mock A服务本身可能也比较麻烦,涉及到网络IO,端口占用等等。所以进一步的,我们只需把与服务A的交互封装到一个interface中。这样只需要mock这个interface就可以了。 2.2 示例 mock最常见的场景是用于单元测试 完整示例 biz.go type People interface { Say(s string) string } // 业务逻辑 func BizLogic(p People, s string) string { return "People Say:" + p.Say(s) } 在单元测试中,验证我们的业务逻辑 biz_test.go import ( "github.com/stretchr/testify/mock" "testing" ) type FakePeople struct { mock.Mock } func (m *FakePeople) Say(str string) string { args := m.Called(str) return args.String(0) } func TestSomething(t *testing.T) { // create an instance of our test object testObj := new(FakePeople) // setup expectations testObj.On("Say", "Jack").Return("hello Jack") testObj.On("Say", "Lucy").Return("hello Lucy") // call the code we are testing expected := "People Say:hello Jack" got := BizLogic(testObj, "Jack") if expected == got { t.Logf("got = %v; expected = %v", got, expected) } else { t.Errorf("got = %v; expected = %v", got, expected) } } 2.3 实现原理 FakePeople是一个实现了接口People的类。有时候它也被称为Stub。它可以被理解为一个占位符,替代对真实的服务交互过程。 ...

June 24, 2021 · 2 min

玩转consul(5)--大规模部署的性能开销定量分析(补充说明)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 在萌叔的文章玩转consul(3)–大规模部署的性能开销定量分析 中,探讨了consul支持大规模集群可能出现性能瓶颈。引出可以通过拆分consul集群或者逻辑切分DC的方法,来降低consul的压力。本文将展开说明一下。 2. 解释 2.1 单个DC的情况 假设服务A依赖服务B(调用服务B), 服务A和服务B各有200个实例, 服务B注册到consul集群上, 服务A通过consul集群发现服务B。 如果服务B做滚动发布,考虑最坏的情况,每次发布一个实例, consul总共发出的通知数为 200 * 200 = 40,000 2.2 2个DC的情况 拆分成2个DC后,单个DC中,服务A和服务B各有100个实例,限制不允许跨DC调用。如果服务B做滚动发布,考虑最坏的情况,DC1和DC2同时发布,每次发布一个实例,consul总共发出的通知数为 100 * 100 + 100 * 100 = 20,000 3. 总结 当服务的规模很大时,即使拆分成2个DC,服务整体的高可用能力不会受到影响。对比2.1和2.2,consul集群的压力减少了一半。另外咱们之前提过,单个DC中consul集群(server)的规模不宜过大,否则会影响日志复制、还有选主的速度。拆分到2个DC后,原有的consul集群也被拆分成了2个集群,单个DC中consul集群的规模下降了,这样还给我们留出扩容的余地。 后记 2022年1月29日 用单元化的思想(见参考资料1、2)来解决注册中心压力大的问题与我文章中提到的,划分逻辑DC的思想不谋而合。 参考资料 1.单元化架构解决了什么问题 2.单元化介绍 请我喝瓶饮料

June 16, 2021 · 1 min

读写锁为什么那么快?(2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.前言 在上一篇文章读写锁为什么那么快?(1)中,萌叔探讨了读写锁提高程序性能的原因。这一篇文章,萌叔将聊聊读写锁面临的2个问题。并看看标准库的是如何实现。 2.几个问题 Q1: 如何避免写锁饥饿或者读锁饥饿问题? 我们知道读写锁的特性:同一时刻允许多个线程(协程)对共享资源进行读操作, 也就是说多个线程(协程)可以多次加读锁。那么会出现这样一种情况,读锁还没有被完全释放的情况(读锁计数器大于0),又有新的读操作请求加读锁。这样写协程永远无法加上写锁,将会一直阻塞。 A1: 标准库实际上引入某种类似排队的机制 如上图所示,协程G1、G2、G3、G4并发访问共享资源。它们请求加锁的顺序为矩形的左边沿,耗时为矩形块的长度 t1时刻,G1加上了读锁,开始读操作 t2时刻,G2也加上了读锁,进行读操作 t3时刻,G3表示它要进行写操作 t4时刻,G4也想加读锁,并且现在共享资源上施加的读锁还没有完全释放(需要到t5时刻才能释放),但是由于G3已经表示它需要加写锁了,所以G4将会阻塞 所以实际的运行顺序是 Q2: 如何确保加读锁时,成本尽可能的低? 读写锁最适用的情况是读多写少的场景,降低读锁的加锁和释放锁的开销,才能提高整体的吞吐能力。 A2: 读协程和写协程互斥仅用的到int32的整数 萌叔虽然也使用condition实现了读写锁,但由于 condition要求在判断条件是否满足,以及修改条件中相关联的变量时,都需要加互斥锁,因此萌叔的实现性能和标准库有一定的差距。 写协程和写协程的互斥靠的是标准库的互斥锁 3.代码 标准库的实现很巧妙,阅读有一定的难度。萌叔将对部分代码进行注释,并讲解。 package sync import ( "internal/race" "sync/atomic" "unsafe" ) type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers /* 用于标识 1. 是否已经加了读锁或者写锁 2. 有多少个协程想加读锁(包含已经加上读锁的) */ readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers /* 用于标识 写协程需要等待多少个读协程释放读锁才可以加上写锁 */ readerWait int32 // number of departing readers } const rwmutexMaxReaders = 1 << 30 func (rw *RWMutex) RLock() { ... /* rw.readerCount+1 表示有读协程想加读锁 rw.readerCount < 0 就说明当前共享资源已经被加了写锁,或者排队的时候,读操作排在了某个写操作请求的后面,那么需要把自己挂起在rw.readerSem上 */ if atomic.AddInt32(&rw.readerCount, 1) < 0 { // A writer is pending, wait for it. runtime_SemacquireMutex(&rw.readerSem, false, 0) } ... } func (rw *RWMutex) RUnlock() { ... /* rw.readerCount 小于0 说明有写协程想要加写锁 则需要唤醒写协程 */ if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 { // Outlined slow-path to allow the fast-path to be inlined rw.rUnlockSlow(r) } ... } func (rw *RWMutex) rUnlockSlow(r int32) { // A writer is pending. if atomic.AddInt32(&rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem, false, 1) } } func (rw *RWMutex) Lock() { ... // First, resolve competition with other writers. /* 写协程和写协程的互斥依靠标准库的互斥锁 */ rw.w.Lock() // Announce to readers there is a pending writer. /* */ r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders // Wait for active readers. /* 只要不等于0,就说明当前共享资源已经被加了读锁 那就把自己挂起在rw.writerSem上 */ if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 { runtime_SemacquireMutex(&rw.writerSem, false, 0) } ... } func (rw *RWMutex) Unlock() { ... // Announce to readers there is no active writer. r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders) ... // Unblock blocked readers, if any. /* 只要有读协程想要加读锁,就唤醒读协程 */ for i := 0; i < int(r); i++ { runtime_Semrelease(&rw.readerSem, false, 0) } // Allow other writers to proceed. rw.w.Unlock() ... } Golang的标准库的读写锁实现具有某种偏向性(偏向读操作)。 在施加写锁期间,只要有读协程表示想加读锁,等到写锁释放时。不管是否有其它写协程想要加写锁。都会优先把共享资源,让给读协程。 ...

June 1, 2021 · 2 min

读写锁为什么那么快?(1)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.实验 首先让我们来看一组单元测试结果,看看互斥锁和读写锁在不同场景下的表现。 这里使用了极客兔兔的《Go 语言高性能编程》中的case,在此特别鸣谢 vearne/golab BenchmarkReadMore-4 1 1195229024 ns/op BenchmarkReadMoreRW-4 9 122810085 ns/op BenchmarkReadMoreMyRW-4 8 145523134 ns/op BenchmarkWriteMore-4 1 1296582090 ns/op BenchmarkWriteMoreRW-4 1 1194438009 ns/op BenchmarkWriteMoreMyRW-4 1 1247918412 ns/op BenchmarkEqual-4 1 1344248968 ns/op BenchmarkEqualRW-4 2 661674534 ns/op BenchmarkEqualMyRW-4 2 685353924 ns/op 1.1 读写量 9:1 读多写少 方法 说明 耗时(ms) 备注 BenchmarkReadMore 标准库-互斥锁 1195 BenchmarkReadMoreRW 标准库-读写锁 122 BenchmarkReadMoreMyRW 萌叔实现的读写锁 145 传送门: vearne/second-realize/rwlock 1.2 读写量 1:9 写多读少 ...

June 1, 2021 · 1 min

grpc的反射机制

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 读者们可能使用过 fullstorydev/grpcurl 来对grpc服务进行调用调试。 假定有一个简单的grpc的服务 main.go package main import ( "context" "log" "net" "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" "google.golang.org/grpc/reflection" ) const ( port = ":50051" ) // server is used to implement helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() helloService := &server{} pb.RegisterGreeterServer(s, helloService) // 注册反射服务 // Register reflection service on gRPC server. reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } 使用grpcurl来请求grpc服务 ...

May 11, 2021 · 2 min

聊聊Raft协议

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 参考资料 1.1 In Search of an Understandable Consensus Algorithm 1.2 寻找一种易于理解的一致性算法 1.3 Raft协议精解 1.4 Raft协议动画演示 1.5 goraft/raftd The original project authors have created new raft implementations now used in etcd and InfluxDB. goraft/raftd的作者参与了etcd项目的实现,所以goraft/raftd是有参考价值的 。另外goraft/raftd的实现不完整,没有实现Log擦除等功能,因此不能用于生产环境。 2. Node的简单介绍 2.1 node的三种状态(state) 图1 有限状态自动机 Leader Candidate Follower 2.2 各种存储 2.2.1 Write-Ahead Log(WAL) 存储:文件 4f raft:join">{"name":"2832bfa","connectionString":"http://localhost:4001"} e raft:nop 4f raft:join">{"name":"3320b68","connectionString":"http://localhost:4002"} 4f raft:join">{"name":"7bd5bdc","connectionString":"http://localhost:4003"} e raft:nop 29 write"{"key":"foo","value":"bar"} 29 write"{"key":"aaa","value":"bbb"} 29 write"{"key":"bbb","value":"ccc"} 29 write"{"key":"ddd","value":"eee"} e raft:nop e raft:nop 29 write"{"key":"foo","value":"bar"} 29 write"{"key":"foo","value":"bar"} 2.2.2 状态信息(状态信息) commitIndex、peer等 ...

April 13, 2021 · 2 min

由grafana-image-renderer引出的一个问题

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 前段时间利用grafana/grafana-image-renderer 做了一个自动截图服务。 见萌叔的文章抓取GRAFANA PANEL视图。但是我发现了一个问题(grafana版本v6.0.1),如果grafana的实例数量超过1个,在grafana中访问grafana render link时,就会有一定的概率失败,图片无法成功渲染。这到底是是为什么? 本文将给出完整的问题排查和解决过程,希望读者可以从中汲取一些养分,为以后排查其它问题提供一些思路。 2. 日志,一切从日志开始 看到这种问题,首先看看日志中是否有线索 2.1 首先看grafana的日志 只能了解到点击grafana render link之后,会触发一个GET请求,访问grafana服务的 /render/d-solo/P3a2p0cZz/redis-and-mysql-and-cache,然后引起一个内部错误 2.2 看看grafana-image-renderer的日志 我们已经知道,图片渲染的过程,grafana要调用grafana-image-renderer,让我们来看下grafana-image-renderer的日志。 需要注意,grafana-image-renderer的日志默认是打印在标准输出和标准错误输出里的。 对比成功和失败的case我发现,在失败的case中,日志中会多了一条401 (Unauthorized)的日志 对应的链接,就是我们要渲染成图片的panel所对应的网页地址 猜测1 现在我们可以猜测grafana-image-renderer可能是请求grafana服务对应的网页资源失败,然后导致图片渲染失败。 2.3 抓包,需要API入口 2.3.1 对grafana-image-renderer抓包 为了进一步验证这个上面的猜测,抓包看一下,grafana-image-renderer所收到的请求 萌叔抓包使用的是buger/goreplay ./gor --input-raw :8081 --output-stdout 1 1a1a5c7c2e733bf0ed41f7ce35bee28da845ab49 1616730655714291898 2541293 GET /render?domain=grafana.example.com&encoding=&height=500&renderKey=HPXorbVBhC6taLHFEe6Jg9O3e5w6R5xn&timeout=60&timezone=Asia%2FShanghai&url=http%3a%2f%2fgrafana.example.com%2fd-solo%2fP3a2p0cZz%2fredis-and-mysql-and-cache%3forgId%3d1%26from%3d1616719854090%26to%3d1616730654091%26var-instance%3d192.168.1.100%3a9090%26panelId%3d8%26width%3d1000%26height%3d500%26tz%3dAsia%2fShanghai%26render%3d1&width=1000 HTTP/1.1 X-Forwarded-Proto: http Host: grafana-image-renderer.example.com Connection: keep-alive User-Agent: Go-http-client/1.1 Accept-Encoding: gzip 展开来看 将url参数解码以后,确实就是grafana中panel对应的网页地址, 到这里猜测1,已经被证实了。 猜测2 请求的参数中有个renderKey非常的可疑,它会不会是请求grafana获取对应网页时,用来鉴权的token呢? 2.3.2 对grafana抓包 这里使用tcpdump抓取之后,然后用wireshark中观察 tcpdump -i eth0 host 192.168.1.102 and port 3000 -w /tmp/xxx.cap grafana-image-renderer请求grafana时,确实在Cookie中携带了renderkey。离真相越来越近了。 ...

March 26, 2021 · 3 min

一个关于go module的有趣话题

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 假如你正在使用go mod管理某个项目的代码库依赖 case1: 某个项目的tag是超过v1版本的,你该怎么定义module case2: 某个项目要同时进行2个版本的开发,v2和v3 你该怎么定义module 2. 问题 为了验证效果,萌叔创建了项目 vearne/mod-multi-version 这个代码库只有一个文件 package mmv import "fmt" const Versoin = "v1.0.0" func PrintVersion(){ fmt.Println("version:", Versoin) } 其它项目使用vearne/mod-multi-version package main import ( "github.com/vearne/mod-multi-version" ) func main() { mmv.PrintVersion() } 代码库的tag列表中包含以下tag v1.0.0 v1.0.2 v2.0.1-Alpha v2.0.5 v2.0.6 v3.0.0 v3.0.1 但如果你试图拉取v2.0.1-Alpha ╰─$ go get github.com/vearne/mod-multi-version@v2.0.1-Alpha go get: github.com/vearne/mod-multi-version@v2.0.1-Alpha: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2 错误提示主版本号只能是v0或者v1 ...

February 25, 2021 · 1 min

抓取Grafana Panel视图

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 最近萌叔在做监控系统的改造,其中一个目标是,当Prometheus触发告警之后需要将Alert Rule对应的grafana图表以邮件的形式发送给处理人员。 2. grafana/grafana-image-renderer 经过搜索资料,萌叔发现grafana官方出了一个插件,可以直接用于抓取grafana的Panel图表。 传送门: grafana/grafana-image-renderer 它有2种运行方式 以grafana的插件方式运行 以外部服务的形式运行 这里以第2种方式展开。 2.1 docker模式运行 这个外部服务是一个nodejs的程序 node build/app.js server --config=config.json puppeteer很像,它接收一个URL地址作为参数,然后运行Headless Chrome,向目标地址发起请求,解析并渲染网页,最后截图。 Headless Chrome可以这样理解,无需显卡、显示器,Chrome以命令行程序的方式运行对网页的请求、渲染、截图等工作。详见参考资料4 Grafana官方提供了docker镜像 grafana/grafana-image-renderer 直接运行即可 2.2 Grafana配置 [rendering] # grafana image renderer服务地址 server_url = http://grafana-image-renderer.example.com:8081/render # grafana服务地址 callback_url = http://grafana.example.com:3000/ 3. 获取图片地址 点击share 蓝色区域是Panel的网页地址,红色区域是Panel的图片地址 图片地址形如: http://dev2:3000/render/d-solo/TBeJbKBmz/fake_service?refresh=1m&panelId=6&orgId=1&from=1611216312639&to=1611218112639&theme=dark&width=1000&height=500&tz=Asia%2FShanghai 注意: from、to参数可能需要修改 6小时图表 now-6h 至 now http://dev2:3000/render/d-solo/TBeJbKBmz/fake_service?refresh=1m&panelId=6&orgId=1&from=now-6h&to=now&theme=dark&width=1000&height=500&tz=Asia%2FShanghai 2天图表 now-2h 至 now http://dev2:3000/render/d-solo/TBeJbKBmz/fake_service?refresh=1m&panelId=6&orgId=1&from=now-2d&to=now&theme=dark&width=1000&height=500&tz=Asia%2FShanghai 4.总结 webhook服务只需要在alert被触发时,请求对应的图片地址,下载图片,再将图片打包在邮件中发送出来即可。 参考资料 puppeteer/puppeteer grafana/grafana-image-renderer Grafana Image Renderer headless chrome 请我喝瓶饮料

January 21, 2021 · 1 min