go-resty/resty中Trace Info说明

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 今天萌叔翻看Golang的request库go-resty/resty,看了请求trace Info的相关的细节。 程序如下: package main import ( "fmt" "github.com/go-resty/resty/v2" ) func main() { target := "https://vearne.cc/" client := resty.New() resp, err := client.R(). EnableTrace(). Get(target) if err != nil { fmt.Println("error:", err) return } fmt.Println(len(resp.String()), resp.StatusCode()) // Explore trace info fmt.Println("Request Trace Info:") ti := resp.Request.TraceInfo() fmt.Println(" DNSLookup :", ti.DNSLookup) fmt.Println(" ConnTime :", ti.ConnTime) fmt.Println(" TCPConnTime :", ti.TCPConnTime) fmt.Println(" TLSHandshake :", ti.TLSHandshake) fmt.Println(" ServerTime :", ti.ServerTime) fmt.Println(" ResponseTime :", ti.ResponseTime) fmt.Println(" TotalTime :", ti.TotalTime) fmt.Println(" IsConnReused :", ti.IsConnReused) fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle) fmt.Println(" ConnIdleTime :", ti.ConnIdleTime) fmt.Println(" RequestAttempt:", ti.RequestAttempt) fmt.Println(" RemoteAddr :", ti.RemoteAddr.String()) } 输出: ...

July 22, 2022 · 1 min

从http.Transport看连接池的设计

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 本文基于Golang 1.17.6 1.前言 之前萌叔曾在文章 imroc/req 连接池使用须知 提及过Golang标准库net/http提供的连接池http.Transport,但是是浅尝辄止。 本文萌叔想从http.Transport出发来谈谈一个连接池设计应该考虑哪些问题? 2.连接池的功能特征 下图是针对Grpc和Thrift压测结果(见参考资料1)。我么可以看出,长连接相比与短连接相比,QPS大概提升了1倍多。这是因为长连接减少连接建立的所需的3次握手。 要对长连接进行管理,特别是对闲置的长连接进行管理,就不可避免的引入连接池。 特征 需要一个连接时,并不一定真的创建新连接,而是优先尝试从连接池选出空闲连接;如果连接池对应的连接为空,才创建新连接。 销毁并不是真的销毁,而是将使用完毕的连接放回连接池中(逻辑关闭)。 这里引出了几个问题。 Q1:获取连接阶段,我们有没有办法知道从连接池中取出的空闲连接(复用)是有效的,还是无效的? Q2:把使用完毕的连接放回连接池的阶段,空闲连接数量是否要做上限的约束。如果空闲连接数量有上限约束且空闲连接的数量已经达到上限。那么把连接放回连接池的过程,必然需要将之前的某个空闲连接进行关闭,那么按照什么规则选择这个需要关闭的连接。 Q3:放置在连接池中的连接,随着时间的流逝,它可能会变成无效连接(stale)。比如由于Server端定时清理空闲连接。那么为了确保连接池中连接的有效性,是否需要引入定时的检查逻辑? 3.net/http中连接池的实现 net/http中连接池的实现代码在 net/http/transport.go 中 获取连接 Transport.RoundTrip() -> Transport.getConn() 放回连接(逻辑关闭) Response.Body.Close() -> bodyEOFSignal.Close() -> Transport.tryPutIdleConn() 为了约束空闲连接的数量,连接池引入了几个变量: MaxIdleConns MaxIdleConns controls the maximum number of idle (keep-alive) connections across all hosts. 所有host总的最大空闲连接数量,默认值是100 MaxIdleConnsPerHostif non-zero, controls the maximum idle (keep-alive) connections to keep per-host。 针对每个Host能够保持的最大空闲连接数量。默认值是2 这个是一个比较有意思的变量,因为默认情况,所有的HTTP请求都使用同一个连接池,由于MaxIdleConns存在,如果针对某个Host的 连接占用了大量空间,那么针对其它Host的连接可能就没有存储空间了。 MaxConnsPerHost MaxConnsPerHost optionally limits the total number of connections per host, including connections in the dialing, active, and idle states. 默认值是0,表示不限制。 ...

June 20, 2022 · 2 min

Golang协程调度

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 本文基于go1.17.6 1.什么是协程? 协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』, 具有对内核来说不可见的特性。 1.1 协程的特点 线程的切换由操作系统负责调度,协程由用户自己进行调度 线程的默认Stack大小是MB级别,而协程更轻量,接近KB级别。 在同一个线程上的多个协程,访问某些资源可以不需要锁 适用于被阻塞的,且需要大量并发的场景。 1.2 Golang的GMP模型 CPU驱动线程上的任务执行 线程由操作系统内核进行调度,Goroutine由Golang运行时(runtime)进行调度 P的 local runnable queue是无锁的,global runnable queue是有锁的 P的 local runnable queue长度限制为256 注意: M和P是绑定关系 M和G是绑定关系 P只是暂存G,他们之间不是绑定关系 E-R图 简化后的E-R图 注意: 后面为了书写简单直接将 local runnable queue表示为本地队列 global runnable queue表示为全局队列 延伸 timer的四叉堆和内存分配器使用的mcache也是每个P一个 Q: 为什么默认情况下P的数量与CPU数量一致? A: 这样可以避免把CPU时间浪费在上线文切换上 1.3 协程和线程的资源消耗对比 类别 栈内存 上下文切换 备注 Thread 1MB 1us 内存占用使用的是Java线程的默认栈大小 Goroutine 4KB 0.2us 内存占用使用的是Linux+x86下的栈大小 2. 常见的Goroutine 让出/调度/抢占场景 ...

March 25, 2022 · 5 min

聊聊Golang中形形色色的同步原语

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 注意: 本文基于Golang 1.17.1 1.引言 阅读Golang的源码的话,会发现里面有形形色色的同步原语,发挥着重要的作用。这篇文章,萌叔打算把它们之间的依赖关系,以及与Linux的同步原语的对应关系整理一下。 2. 与Linux系统的同步原语之间的对应关系 锁类型 Golang Linux 互斥锁 runtime.mutex (低阶) sync.Mutex (高阶) #include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex); 信号量 runtime.semaphore #include <sys/sem.h>int semget(key_t key, int nsems, int semflg);int semctl(int semid, int semnum, int cmd, …);int semop(int semid, struct sembuf *sops, size_t nsops); 条件变量 sync.Cond #include <pthread.h>int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond); 3.依赖关系 3.1 runtime.mutex runtime/lock_futex.go ...

January 5, 2022 · 1 min

为什么Golang的Timer实现使用四叉堆而不是二叉堆

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 Golang在Timer(计时器)的实现中使用了四叉堆,而不是常见的二叉堆,这个问题引起了我的兴趣。 对于这个问题,网上的解释是这样的 但是我仔细查阅了资料以后,有了点有趣的发现。 2.说明 假设: 堆为d叉堆,堆中共有n个节点 备注: 时间复杂度的证明见d-ary heap Timer最常见的2个操作 1)Insert() 添加一个计时器。涉及 sift-up(上推)操作,时间复杂度为O(log n / log d) Python代码 import math result = math.log(n, d) 时间消耗(需要交换或者比较的次数) n(数量) 二叉堆 四叉堆 八叉堆 1w 13.3 6.64 4.43 10w 16.61 8.30 5.54 100w 19.93 9.97 6.64 1000w 23.25 11.63 7.75 2) Delete() 指定时间到达后,清除计时器。涉及 sift-down(下推)操作,时间复杂度为O(d * log n / log d) Python代码 import math result = d * math.log(n, d) 时间消耗(需要交换或者比较的次数) n(数量) 二叉堆 四叉堆 八叉堆 1w 26.58 26.58 35.43 10w 33.33 33.33 44.29 100w 39.86 39.86 53.15 1000w 46.51 46.51 62.01 3. 结论 正常情况下,插入操作:多叉堆执行性能优于二叉堆; 删除操作:多叉堆执行性能弱于二叉堆。 但是细心的朋友应该已经发现,四叉堆插入操作的耗时只有二叉堆的一半,而删除操作的耗时与二叉堆完全相同。 这么一对比,优势确实显著。 ...

December 28, 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 &quot;People Say:&quot; + p.Say(s) } 在单元测试中,验证我们的业务逻辑 biz_test.go import ( &quot;github.com/stretchr/testify/mock&quot; &quot;testing&quot; ) 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(&quot;Say&quot;, &quot;Jack&quot;).Return(&quot;hello Jack&quot;) testObj.On(&quot;Say&quot;, &quot;Lucy&quot;).Return(&quot;hello Lucy&quot;) // call the code we are testing expected := &quot;People Say:hello Jack&quot; got := BizLogic(testObj, &quot;Jack&quot;) if expected == got { t.Logf(&quot;got = %v; expected = %v&quot;, got, expected) } else { t.Errorf(&quot;got = %v; expected = %v&quot;, got, expected) } } 2.3 实现原理 FakePeople是一个实现了接口People的类。有时候它也被称为Stub。它可以被理解为一个占位符,替代对真实的服务交互过程。 ...

June 24, 2021 · 2 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 ( &quot;internal/race&quot; &quot;sync/atomic&quot; &quot;unsafe&quot; ) 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 &lt;&lt; 30 func (rw *RWMutex) RLock() { ... /* rw.readerCount+1 表示有读协程想加读锁 rw.readerCount &lt; 0 就说明当前共享资源已经被加了写锁,或者排队的时候,读操作排在了某个写操作请求的后面,那么需要把自己挂起在rw.readerSem上 */ if atomic.AddInt32(&amp;rw.readerCount, 1) &lt; 0 { // A writer is pending, wait for it. runtime_SemacquireMutex(&amp;rw.readerSem, false, 0) } ... } func (rw *RWMutex) RUnlock() { ... /* rw.readerCount 小于0 说明有写协程想要加写锁 则需要唤醒写协程 */ if r := atomic.AddInt32(&amp;rw.readerCount, -1); r &lt; 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(&amp;rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&amp;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(&amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders // Wait for active readers. /* 只要不等于0,就说明当前共享资源已经被加了读锁 那就把自己挂起在rw.writerSem上 */ if r != 0 &amp;&amp; atomic.AddInt32(&amp;rw.readerWait, r) != 0 { runtime_SemacquireMutex(&amp;rw.writerSem, false, 0) } ... } func (rw *RWMutex) Unlock() { ... // Announce to readers there is no active writer. r := atomic.AddInt32(&amp;rw.readerCount, rwmutexMaxReaders) ... // Unblock blocked readers, if any. /* 只要有读协程想要加读锁,就唤醒读协程 */ for i := 0; i &lt; int(r); i++ { runtime_Semrelease(&amp;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 ( &quot;context&quot; &quot;log&quot; &quot;net&quot; &quot;google.golang.org/grpc&quot; pb &quot;google.golang.org/grpc/examples/helloworld/helloworld&quot; &quot;google.golang.org/grpc/reflection&quot; ) const ( port = &quot;:50051&quot; ) // 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(&quot;Received: %v&quot;, in.GetName()) return &amp;pb.HelloReply{Message: &quot;Hello &quot; + in.GetName()}, nil } func main() { lis, err := net.Listen(&quot;tcp&quot;, port) if err != nil { log.Fatalf(&quot;failed to listen: %v&quot;, err) } s := grpc.NewServer() helloService := &amp;server{} pb.RegisterGreeterServer(s, helloService) // 注册反射服务 // Register reflection service on gRPC server. reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf(&quot;failed to serve: %v&quot;, err) } } 使用grpcurl来请求grpc服务 ...

May 11, 2021 · 2 min

利用docker实现Golang程序的交叉编译

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 萌叔有有一个开源项目vearne/passwordbox,用于密码的管理。笔者想法是在Mac上build出多个平台下的bin文件,这样用户,可以无需自己编译,直接使用编译好的bin文件。 可是 vearne/passwordbox 内部依赖了 mattn/go-sqlite3。这个库编译时,依赖操作系统上的共享库,无法直接进行交叉编译。 2. 解决 前段时间,萌叔在阅读buger/goreplay源码时,偶然发现它有一个思路是利用docker来实现Golang交叉编译。于是笔者借鉴了它的思路,修改了passwordbox Makefile。 现在在Mac上执行 make docker-img # 只需要执行一次,生成基础镜像即可 make release 就可以同时生成Mac和linux下的bin文件 pwbox-v0.0.10-darwin-amd64.tar.gz pwbox-v0.0.10-linux-amd64.tar.gz 完整代码见Makefile 2.1 build一个基础镜像用于linux环境的编译 docker-img: docker build --rm -t $(CONTAINER) -f Dockerfile.dev . Dockerfile.dev FROM golang:1.14 RUN apt-get update &amp;&amp; apt-get install vim-common -y WORKDIR /go/src/github.com/vearne/passwordbox/ ADD . /go/src/github.com/vearne/passwordbox/ RUN go get golang:1.14 实际是基于Ubuntu的一个镜像(Linux) 2.2 借助Docker实现跨平台编译 release-linux: docker run -v `pwd`:$(SOURCE_PATH) -t -e GOOS=linux -e GOARCH=amd64 -i $(CONTAINER) go build $(LDFLAGS) -o pwbox tar -zcvf pwbox-$(VERSION)-linux-amd64.tar.gz ./pwbox rm pwbox -v pwd:$(SOURCE_PATH) 是为了把项目的代码直接挂载到docker中,这样就无需每次拷贝代码到docker中 ...

August 3, 2020 · 1 min