介绍一个grpc记录和重放库 vearne/grpcreply-使用篇

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 gRPC协议应用的非常广泛,针对它流量的记录和重放是一个非常常见的需求。但是到目前为止社区中都没有找到合适的库来实现这个目标。 终于它来了。传送门: vearne/grpcreplay 2.介绍 vearne/grpcreplay是一个网络监控工具,可以记录您的 grpc流量(Unary RPC),并将其用于灰度测试、压测或者流量分析。它有2大特点,使用简单,并且可以直接解析Protobuf的请求,并以JSON形式输出。 在使用vearne/grpcreplay前,你必须要知道vearne/grpcreplay的限制条件: 支持解析Unary RPC,不支持streaming RPC Server端必须开启反射 参考GRPC Server Reflection Protocol 3. 编译&使用 确保server所在的主机上已经安装了libpcap 安装libpcap可以参考以下命令: Ubuntu apt-get install libpcap-dev Centos yum install libpcap-devel Mac brew install libpcap 编译 make build 示例 server package main import ( "context" "encoding/json" "github.com/grpc-ecosystem/go-grpc-middleware" pb "github.com/vearne/grpcreplay/example/search_proto" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" "log" "net" "runtime/debug" "time" ) const PORT = "35001" type SearchServer struct{} func (s SearchServer) Search(ctx context.Context, in *pb.SearchRequest) (*pb.SearchResponse, error) { return &pb.SearchResponse{StaffID: 100, StaffName: in.StaffName}, nil } func (s SearchServer) CurrentTime(ctx context.Context, request *pb.TimeRequest) (*pb.TimeResponse, error) { return &pb.TimeResponse{CurrentTime: time.Now().Format(time.RFC3339)}, nil } func main() { opts := []grpc.ServerOption{ //grpc.Creds(c), grpc_middleware.WithUnaryServerChain( RecoveryInterceptor, LoggingInterceptor, ), //grpc.HeaderTableSize(0), //grpc.WithDisableRetry(), } server := grpc.NewServer(opts...) pb.RegisterSearchServiceServer(server, &SearchServer{}) // 注册反射服务(非常重要) // Register reflection service on gRPC server. reflection.Register(server) lis, err := net.Listen("tcp", ":"+PORT) if err != nil { log.Fatalf("net.Listen err: %v", err) } server.Serve(lis) } func LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { log.Printf("gRPC method: %s, %v", info.FullMethod, req) resp, err := handler(ctx, req) bt, _ := json.Marshal(req) log.Println("body", string(bt)) log.Printf("gRPC method: %s, %v", info.FullMethod, resp) return resp, err } func RecoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { defer func() { if e := recover(); e != nil { debug.PrintStack() err = status.Errorf(codes.Internal, "Panic err: %v", e) } }() return handler(ctx, req) } client ...

October 17, 2022 · 2 min

手撸了一个Golang协程池

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 传送门: vearne/executor 0. 引言 本项目受到Java Executors的启发 Executors,并参考了ExecutorService的接口设计。 支持取消单个任务,或者取消整个集群的任务。 并提供了一个可以动态扩容和缩容的实现DynamicGPool。 欢迎提PR。下面是更详细的说明: 1. 特性 支持取消单个任务,也可以取消协程池上的所有任务 Future.Cancel() ExecutorService.Cancel() 你也可以使用context.Context取消task或者pool。 可以创建多种不同类型的协程池(SingleGPool|FixedGPool|DynamicGPool) 2. 多种类型的协程池 类别 说明 备注 SingleGPool 单个worker协程池 FixedGPool 固定数量worker协程池 DynamicGPool worker数量可以动态变化的协程池 min:最少协程数量 max:最大协程数量 2.1 SingleGPool NewSingleGPool(ctx context.Context, opts ...option) ExecutorService 2.2 FixedGPool NewFixedGPool(ctx context.Context, size int, opts ...option) ExecutorService 2.3 DynamicGPool NewDynamicGPool(ctx context.Context, min int, max int, opts ...dynamicOption) ExecutorService 2.3.1 扩容规则 提交任务时,如果任务队列已经满了,则尝试增加worker去执行任务。 2.3.2 缩容规则: 条件: 如果处于忙碌状态的worker少于worker总数的1/4,则认为满足条件 执行meetCondNum次连续检测,每次间隔detectInterval。如果每次都满足条件,触发缩容。 缩容动作尝试减少一半的worker 3. 注意 由于executor使用了channel作为作为任务队列,所以提交任务时,可能会发生阻塞。 ...

September 26, 2022 · 2 min

Golang 模糊测试 简明教程

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 Golang1.18 引入了一个模糊测试的功能。文本萌叔将会简单介绍下这个特性。 2.模糊测试 传送门 比如我们编写了一个工具包来做除法 package fuzz func Div(a, b int) int { return a / b } 由于疏忽忽略了某些边界条件。显然在上面的代码中,b不能等于0,否则会发生运行时错误。 普通的单元测试 func TestDiv(t *testing.T) { testcases := []struct { a, b, want int }{ {10, 2, 5}, {5, 3, 1}, {-6, 3, -2}, {-6, -3, 2}, } for _, tc := range testcases { result := Div(tc.a, tc.b) if Div(tc.a, tc.b) != tc.want { t.Errorf("Div: %q, want %q", result, tc.want) } } } 由于对分支条件和异常情况考虑的不周全,自测的单元测试能够正常通过。但是一旦b=0,程序将会崩溃。 ...

September 14, 2022 · 1 min

Golang embed(内嵌)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 引言 Golang编译成单个bin文件,发布的时候相当方便了。但是某个配置文件没有办法直接打在bin文件中,比如SSL的密钥,html模板等等。 Golang 1.16 标准库引入了embed包,它可以在编译期间将外部文件嵌入到bin包中。 嵌入的文件都是只读的,因此是协程安全的。 另外由于编译期间嵌入的,因此如果文件发生变化,需要重新编译。 2. 示例 2.2 示例1 嵌入文件夹 传送门 . ├── html │ ├── a.html │ └── index.html └── main.go package main import ( "embed" "log" "net/http" ) //go:embed html var content embed.FS func main() { mutex := http.NewServeMux() mutex.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content)))) err := http.ListenAndServe(":8080", mutex) if err != nil { log.Fatal(err) } } 2.2 示例2 嵌入模板文件 传送门 . ├── main.go └── template ├── hello.tpl └── hello2.tpl package main import ( "bytes" "embed" "html/template" "log" ) //go:embed template/*.tpl var mytpl embed.FS func main() { check := func(err error) { if err != nil { log.Fatal(err) } } list, err := mytpl.ReadDir("template") check(err) for _, item := range list { log.Println("template/" + item.Name()) } data, err := mytpl.ReadFile("template/hello.tpl") log.Println("data:", string(data)) check(err) t, err := template.New("hello").Parse(string(data)) content := bytes.NewBufferString("") err = t.Execute(content, map[string]string{"name": "张三"}) check(err) log.Println(content.String()) } 2.3 示例3 嵌入字符串/字节数据 传送门 ...

September 2, 2022 · 1 min

REDIS-CLUSTER集群slot迁移过程分析

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.前言 在前面的文章中, 《REDIS-CLUSTER集群创建内部细节详解》 萌叔创建一个Redis集群。 这篇文章,我会为集群添加2个节点,并介绍slot的迁移过程。 2.集群扩容 添加主节点127.0.0.1:7006 redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 添加从节点127.0.0.1:7007(并指定其Master节点) redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000 --cluster-slave --cluster-master-id 86f3cb72813a2d07711b56b3143ff727911f4e1e 新添加的节点上并没有slot分布,需要通过命令让slot重新分布 root@BJ-02:~/cluster-test/7007# redis-cli --cluster reshard 127.0.0.1:7000 >>> Performing Cluster Check (using node 127.0.0.1:7000) S: 7eb7ceb4d886580c6d122e7fd92e436594cc105e 127.0.0.1:7000 slots: (0 slots) slave replicates be905740b96469fc6f20339fc9898e153c06d497 M: 86f3cb72813a2d07711b56b3143ff727911f4e1e 127.0.0.1:7006 slots: (0 slots) master 1 additional replica(s) M: be905740b96469fc6f20339fc9898e153c06d497 127.0.0.1:7005 slots:[0-5460] (5461 slots) master 1 additional replica(s) S: 9e29dd4b2a7318e0e29a48ae4b37a7bd5ea0a828 127.0.0.1:7007 slots: (0 slots) slave replicates 86f3cb72813a2d07711b56b3143ff727911f4e1e S: 603a8a403536f625f53467881f5f78def9bd46e5 127.0.0.1:7003 slots: (0 slots) slave replicates 784fa4b720213b0e2b51a4542469f5e318e8658b M: 4e0e4be1b4afd2cd1d10166a6788449dd812a4c0 127.0.0.1:7002 slots:[10923-16383] (5461 slots) master 1 additional replica(s) S: 585c7df69fb267941a40611bbd8ed90349b49175 127.0.0.1:7004 slots: (0 slots) slave replicates 4e0e4be1b4afd2cd1d10166a6788449dd812a4c0 M: 784fa4b720213b0e2b51a4542469f5e318e8658b 127.0.0.1:7001 slots:[5461-10922] (5462 slots) master 1 additional replica(s) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. How many slots do you want to move (from 1 to 16384)? 0 How many slots do you want to move (from 1 to 16384)? 1 What is the receiving node ID? 86f3cb72813a2d07711b56b3143ff727911f4e1e Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1: all Ready to move 1 slots. Source nodes: M: be905740b96469fc6f20339fc9898e153c06d497 127.0.0.1:7005 slots:[0-5460] (5461 slots) master 1 additional replica(s) M: 4e0e4be1b4afd2cd1d10166a6788449dd812a4c0 127.0.0.1:7002 slots:[10923-16383] (5461 slots) master 1 additional replica(s) M: 784fa4b720213b0e2b51a4542469f5e318e8658b 127.0.0.1:7001 slots:[5461-10922] (5462 slots) master 1 additional replica(s) Destination node: M: 86f3cb72813a2d07711b56b3143ff727911f4e1e 127.0.0.1:7006 slots: (0 slots) master 1 additional replica(s) Resharding plan: Moving slot 5461 from 784fa4b720213b0e2b51a4542469f5e318e8658b Do you want to proceed with the proposed reshard plan (yes/no)? yes Moving slot 5461 from 127.0.0.1:7001 to 127.0.0.1:7006: 3. Slot迁移过程 迁移过程在clusterManagerMoveSlot() 中 ...

June 4, 2022 · 3 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

打印struct中Field内存对齐的小工具

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 因为使用golangci-lint时长有关于内存对齐的告警提示,所以做了一个小工具,可以查看struct中Field在内存中的对齐情况 传送门: vearne/mem-align 2. 使用 package main import ( "github.com/vearne/mem-align" ) type Car struct { flag bool age int32 F1 int8 age2 int32 age3 int16 F2 int64 F3 *int32 InnerStruct struct{ InnerByte byte //InnerStr string } F4 []byte Name string F5 error } func main(){ memalign.PrintStructAlignment(Car{}) } 输出: 相同字符,相同颜色的的字符,表示同一个Field。 后记 其实golang官方在go vet中提供了一个子工具fieldalignment golangci-lint也是使用这个分析器进行内存对齐的检查 安装 fieldalignment go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest 使用-fix参数可以直接修改代码文件 fieldalignment -fix {packagePath} 参考资料 opennota/check mdempsky/maligned Structure size optimization in Golang (alignment/padding) size and alignment guarantees fieldalignment

December 13, 2021 · 1 min

rocketmq分享

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 注意:文中使用的部分图,误将ConsumeQueue写成了ConsumerQueue 1.简介 RocketMQ是一个分布式消息和流数据平台,具有低延迟、高性能、高可靠性、万亿级容量和灵活的可扩展性。RocketMQ是2012年阿里巴巴开源的第三代分布式消息中间件,2016年11月21日,阿里巴巴向Apache软件基金会捐赠了RocketMQ;第二年2月20日,Apache软件基金会宣布Apache RocketMQ成为顶级项目。 2.架构 正常情况,写和读都走Master,Master如果宕机,读可以走Slave 在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一种部署方式,虽然这种模式可以提供一定的高可用性但也存在比较大的缺陷。为了实现新的高可用多副本架构,RockeMQ 最终选用了基于 Raft 协议的 commitlog 存储库 DLedger。 2.1 四种角色 2.1.1 NameServer 存储元数据 topic -> broker 无状态 接收来自broker的心跳 检查与borker的通讯是否过期 Topic路由信息 { "OrderTopicConf": "", "queueDatas": [{ "brokerName": "broker-3", "readQueueNums": 4, "writeQueueNums": 4, "perm": 6, "topicSynFlag": 0 }, { "brokerName": "broker-4", "readQueueNums": 4, "writeQueueNums": 4, "perm": 6, "topicSynFlag": 0 }], "brokerDatas": [{ "cluster": "Default_Cluster", "brokerName": "broker-4", "brokerAddrs": { "0": "192.168.12.123:10911", "1": "192.168.12.127:10911" } }, { "cluster": "Default_Cluster", "brokerName": "broker-3", "brokerAddrs": { "1": "192.168.12.220:10911", "0": "192.168.12.12:10911" } }] } 2.1.2 Producter 有发往broker的心跳(Master) ...

November 25, 2021 · 3 min

Grafana-Variable配置小技巧

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 Dashboard是Grafana中非常重要的概念,每个Dashboard都是一个巨大的看板,在Dashboard上可以配置Panel(图表)。在Dashboard中有个特别的配置–Variables,Variables提供了用户和面板交互,可以动态刷新面板。 有些特殊的场景,需要有些小技巧,否则Variables仍然无法满足我们的要求。 1. Case 1 我要查看不同股票的日线图 1.1 Query SELECT trade_date AS "time", close FROM daily_data where ts_code = "$ts_code" order by trade_date ts_code是股票的代码,显然如果只看股票代码,很难想起这个代码究竟对应的是哪一只股票。 最好的方式是如下图这样,在变量的下拉列表中,显示股票名称,但是在实际的Query中使用股票代码查询。 grafana支持这种玩法,称为text和value 1.2 变量配置 注意需要Grafana的版本在7.4.5 或以上 1.2.1 配置Query select concat(name, "#", ts_code) from target; 这一步会查出形如下面的结果 中国平安#601318.SH 三一重工#600031.SH ... 1.2.2 分离出text和value 配置Regex /(?<text>.*)#(?<value>.*)/ 2. Case 2 我们有一组MySQL实例需要监控,MySQL的数量多达几十个。 但是实际上大家都知道,MySQL是主从的,1个主和多个从构成了1个集群。每个集群单独为1个业务提供服务。 MySQL实例如果这样列出来,萌叔根本不可能知道,这个实例属于哪个集群,支撑那个业务。 我们希望的效果,是先选择partner(业务方),然后再选择再从这个集群中选出关心的实例 2.1 Query 事实上,我们可以通过构建2个Variables来达到这个目标 2.1.1 Query1 label_values(mysql_up, partner) 得到变量partner 2.1.2 Query2 label_values(mysql_up{partner="$partner"}, instance) Query2引用Query1的结果 3. Case 3 萌叔的服务部署在2个机房,机房名称是由k8s的维护定义好的,形如 ...

November 18, 2021 · 1 min

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

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 2.3 连接池的变化情况 如果采用确定性算法来划分子集还需要考虑在有节点宕机或者Backend进行过滚动升级的情况下,引起的连接池中连接的变化情况。 这里先上结论,要保证连接池中的连接不大量的销毁和创建,Backend也需要绑定backendID,以确保对于每个Client选出的子集不发生重大的变化 绑定backendID确保调用Subset时,Backend在backends中的位置不发生变化。(绑定backendID可以利用分布式锁来实现) func Subset(backends []string, clientID, subsetSize int) 我们来看一个反例,假定某个Backend backend-10实例宕机,连接池的变化情况 subset/certainty4/verify.go 结果 所有连接数:2500, 重建的连接总数:1257, 百分比: 50.280000 % 因为一个实例宕机,整个集群的连接池中一半的连接都要重新销毁,重连一次,开销还是非常大的。大家都知道集群做滚动发布,持续时间可能长达数分钟。也就是在这数分钟内Client的连接池可能会频繁的发生销毁和创建,这不得不引起我们的重视。不过好在发布动作在服务运行的整个生命周期占比不大。 3. 效果 最后我们来看下,使用划分子集方案后,带来的实际效果 连接数大概下降了30% 常驻内存大概下降40% 协程数下降30% 令人惊喜的是,GC耗时也有了一定程度的下降 总结 使用划分子集的方案来限制连接池的大小确实有不俗的效果,但也要注意细节和风险点。河豚虽然美味,但也有可能有毒。 参考资料 用subsetting 限制连接池中的连接数量 load balancing datacenter/

August 24, 2021 · 1 min