玩转高性能日志库ZAP (6)-采样

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 uber开源的高性能日志库zap, 除了性能远超logrus之外,还有很多诱人的功能,比如支持日志采样、支持通过HTTP服务动态调整日志级别。本文简单聊一下日志采样。 使用说明 Sampling:Sampling实现了日志的流控功能,或者叫采样配置,主要有两个配置参数,Initial和Thereafter,实现的效果是在1s的时间单位内,如果某个日志级别下同样内容的日志输出数量超过了Initial的数量,那么超过之后,每隔Thereafter的数量,才会再输出一次。是一个对日志输出的保护功能。 注意 这里画个重点 仅对"同样内容" 的日志做采样 默认1s的时间单位内 示例 package main import ( "go.uber.org/zap" ) func main() { config := zap.NewProductionConfig() // 默认值:Initial:100 Thereafter:100 config.Sampling = &zap.SamplingConfig{ Initial: 5, // 从第6条数据开始 Thereafter: 3, // 每3条打印一条 } // 可以置为nil 来关闭采样 //config.Sampling = nil config.Encoding = "console" logger, _ := config.Build() defer logger.Sync() // 打印的消息要**重复**才会被执行采样动作 for i := 0; i < 100; i++ { logger.Info("hello") } } 输出 仅输出36条日志,而不是100条 ...

June 10, 2020 · 2 min

玩转NSQ(1)-鉴权

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 环境 软件版本 NSQ v1.2.0 golang 1.12 go-nsq github.com/nsqio/go-nsq v1.0.7 1.引言 NSQ 是一个基于 Go 语言的分布式实时消息平台,它具有分布式、去中心化的拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。它的吞吐能力非常强,在单实例4核CPU可以达到19wop/s。足以应付日常使用 (见参考资料1) 。 不过它没有内建的数据副本机制(如果某个nsqd宕机,则该nsqd上的消息会丢失),且默认数据也不持久化(需要将mem_queue_size修改为0),使用NSQ仍然需要谨慎。 当我们考查一个消息队列,除了吞吐能力和可靠性,安全性也是重要的考察点。NSQ默认提供了鉴权机制(见查考资料2) 2. 使用鉴权 2.1 启用鉴权&对鉴权服务的要求 启动鉴权 nsqd --auth-http-address=nsqauth.example.com:4181 --http-address=127.0.0.1:4151 NSQ把权限的管理委托给了外部的鉴权服务,通过auth-http-address指定 官方给的鉴权服务的参考代码是 jehiah/nsqauth-contrib 笔者跟据它的思路重新实现了Golang的版本 vearne/go-nsqauthd NSQ要求鉴权服务能够对如下的HTTP进行响应 /auth?secret=xxxx&remote_ip=39.156.69.79&tls=true nsqd会拿着客户端的信息到鉴权服务进行鉴权 secret是客户端提供的秘钥(相当于redis中的密码) remote_ip是客户端的IP地址 tls表示客户端与nsqd之间的通讯是否有TLS保护 鉴权返回形如如下的信息,表明此用户可以操作的topic { "identity": "xxxx", "ttl": 3600, "Authorizations": [ { "channels": [ ".*" ], "topic": "topic1", "permissions": [ "subscribe", // 有topic1上的订阅权限 "publish" // 有topic1上的发布权限 ] } ] } vearne/go-nsqauthd为了简单期间,将秘钥直接存储在了csv文件中。 login ip tls_required topic channel subscribe publish 4BFE467B-FCBA-4519-BAC8-E9A3C57EDEB6 false test .* subscribe publish test_local 127.0.0.1 true local .* subscribe publish test_publish 127.0.0.1/24 false test_publish .* publish 其中login对应secret是秘钥信息, ip可以是某个子网, 如果不写表示没有ip限制,channel和topic都支持通配符的表达式。 ...

September 29, 2019 · 2 min

GOMAXPROCS你设置对了吗?

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 有圈子的朋友介绍 uber-go/automaxprocs, 我才发现之前在docker中, Golang程序设置的GOMAXPROCS不正确,有必要在重新回顾一下了。 2. Go 调度器: M, P 和 G 我们知道在Go scheduler中,G代表goroutine, P代表Logical Processor, M是操作系统线程。在绝大多数时候,其实P的数量和M的数量是相等。 每创建一个p, 就会创建一个对应的M 只有少数情况下,M的数量会大于P golang runtime是有个sysmon的协程,他会轮询的检测所有的P上下文队列,**只要 G-M 的线程长时间在阻塞状态,那么就重新创建一个线程去从runtime P队列里获取任务。先前的阻塞的线程会被游离出去了,当他完成阻塞操作后会触发相关的callback回调,并加入回线程组里。**简单说,如果你没有特意配置runtime.SetMaxThreads,那么在没有可复用的线程的情况下,会一直创建新线程。 3. GOMAXPROCS的取值 3.1 虚拟机和物理机 我们知道可以通过 runtime.GOMAXPROCS() 来了设定P的值 Go 1.5开始, Go的GOMAXPROCS默认值已经设置为 CPU的核数, 这允许我们的Go程序充分使用机器的每一个CPU,最大程度的提高我们程序的并发性能。 但其实对于IO密集型的场景,我们可以把GOMAXPROCS的值超过CPU核数,在笔者维护的某个服务中,将GOMAXPROCS设为CPU核数的2倍,压测结果表明,吞吐能力大概能提升10% 3.2 容器中 在容器中,Golang程序获取的是宿主机的CPU核数导致GOMAXPROCS设置的过大。比如在笔者的服务中,宿主机是48cores,而实际container只有4cores。 线程过多,会增加上线文切换的负担,白白浪费CPU。 uber-go/automaxprocs 可以解决这个问题 package main import ( "fmt" _ "go.uber.org/automaxprocs" "runtime" ) func main() { // Your application logic here. fmt.Println("real GOMAXPROCS", runtime.GOMAXPROCS(-1)) } 另外也可以通过设置环境变量GOMAXPROCS来改变Golang程序的GOMAXPROCS默认值 ...

September 18, 2019 · 1 min

玩转GRPC(1)-整合JSON编码方式

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 引言 提到GRPC大家想到的就是 HTTP2 + protobuf。HTTP2能够实现多路复用, protobuf提高数据传输的压缩率。但其实GRPC还可以跟Thrift、JSON等编码方式进行整合。 gRPC lets you use encoders other than Protobuf. It has no dependency on Protobuf and was specially made to work with a wide variety of environments. We can see that with a little extra boilerplate, we can use any encoder we want. While this post only covered JSON, gRPC is compatible with Thrift, Avro, Flatbuffers, Cap’n Proto, and even raw bytes! gRPC lets you be in control of how your data is handled. (We still recommend Protobuf though due to strong backwards compatibility, type checking, and performance it gives you.) ...

September 2, 2019 · 3 min

聊聊布隆过滤器

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 笔者早几年的工作经历和爬虫相关,为防止重复抓取URL, 要对每个需要抓取的URL进行判重,由于每天待抓取的URL数量都在好几亿条。因此去重服务的压力很大,当时相关的小伙伴就曾经调研过布隆过滤器。 本文笔者将结合Google Guava的BloomFilter(com.google.common.hash.BloomFilter),谈谈布隆过滤器的实现,以及它的一些特点。 2. 布隆过滤器简介 An empty Bloom filter is a bit array of m bits, all set to 0. There must also be k different hash functions defined, each of which maps or hashes some set element to one of the m array positions, generating a uniform random distribution. 布隆过滤器由一个m位的bitArray以及k个hash函数组成。m和k的具体值可以由布隆过滤器中预期存储的数据量n,以及可能出现假阳性概率p决定。 待存储的数据经过1个hash函数作用,并会将bitArray中的某一位置成1 k个hash函数最多会将bitArray中k位置成1 如上图所示,假定k=2 hash_func1将bitArray[3]=1 hash_func2将bitArray[8]=1 如果想判断数值1990是否在布隆过滤器中,只需要重新执行hash_func1和hash_func2,如果第3位和第8位都是1,那么1990可能在布隆过滤器中(存在假阳性的可能),反之第3位和第8位有1位为0,则1990一定不在布隆过滤器中。 可以换一个角度来解释布隆过滤器,每个hash函数都在尝试提取数据值的一部分特征。存储数据的过程(置1),实际是在标记,布隆过滤器中至少包含一个元素满足特定的特征。如果有一个特征不满足,那么该数据一定不在布隆过滤器中。 ...

August 15, 2019 · 3 min

玩转Prometheus(5)-监控Redis和MySQL的工具包(业务层)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 对于高可用的服务,监控的粒度往往都会非常细。如果恰好你也在使用 Prometheus, 也需要在业务层对Redis连接池和MySQL连接池进行监控。那么此篇文章对你而言将是一种福利。 Redis Client go-redis/redis MySQL Client jinzhu/gorm 2. 样例代码 go get github.com/vearne/golib main.go package main import ( "github.com/go-redis/redis" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/vearne/golib/metric" "log" "net/http" "time" ) func main() { // init redis client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", PoolSize: 100, }) // ***监控Redis连接池*** metric.AddRedis(client, "car") // init mysql DSN := "test:xxxx@tcp(localhost:6379)/somebiz?charset=utf8&loc=Asia%2FShanghai&parseTime=true" mysqldb, err := gorm.Open("mysql", DSN) if err != nil { panic(err) } mysqldb.DB().SetMaxIdleConns(50) mysqldb.DB().SetMaxOpenConns(100) mysqldb.DB().SetConnMaxLifetime(5 * time.Minute) // ***监控MySQL连接池*** metric.AddMySQL(mysqldb, "car") // do some thing for i := 0; i < 30; i++ { go func() { for { client.Get("a").String() time.Sleep(200 * time.Millisecond) mysqldb.Exec("show tables") } }() } http.Handle("/metrics", promhttp.Handler()) log.Fatal(http.ListenAndServe(":9090", nil)) log.Println("starting...") } func AddRedis(client RedisClient, role string) func AddMySQL(client *gorm.DB, role string) role 仅用于区分不同的Redis实例 ...

July 1, 2019 · 2 min

gin的timeout middleware实现(续)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 在笔者的上一篇文章中,我们探讨了如何开发一个对业务无侵入的timeout middleware的实现,但是遗留了问题。在超时发生时,后台运行的子协程可能会不断累积,造成协程的泄露,最终引发程序奔溃。 2. 解决 为了解决子协程退出的问题,我们需要在超时发生时,通知子协程,让其也尽快退出。 下面的例子中,gin的处理函数long(c *gin.Context)中有对gRPC服务的调用 我们使用grpc/grpc-go 中提供的 greeter_server来提供gRPC服务 // Package main implements a server for Greeter service. package main import ( "context" "log" "net" "time" "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" ) const ( port = ":50051" ) // server is used to implement helloworld.GreeterServer. type server struct{} // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.Name) time.Sleep(2*time.Second) return &pb.HelloReply{Message: "Hello " + in.Name}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } gin相关代码 main.go ...

May 20, 2019 · 3 min

gin的timeout middleware实现

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 说到Golang中应用最广泛的web框架,恐怕非gin-gonic/gin莫属了。在服务中,如果它依赖的后端服务出现异常,我们希望错误能够快速的暴露给调用方,而部署无限期的等待。我们需要一个timeout middleware, 来完成这个目标。 对于gin框架,timeout middleware可参考的资料,比较多见的是参考资料1,但是它的实现,对业务代码造成了入侵,不是很友好,这里给出笔者的实现,供大家参考 2. 实现 直接上代码 main.go package main import ( "bytes" "github.com/gin-gonic/gin" "github.com/vearne/golib/buffpool" "log" "net/http" "time" ) type SimplebodyWriter struct { gin.ResponseWriter body *bytes.Buffer } func (w SimplebodyWriter) Write(b []byte) (int, error) { return w.body.Write(b) } func Timeout(t time.Duration) gin.HandlerFunc { return func(c *gin.Context) { // sync.Pool buffer := buffpool.GetBuff() blw := &SimplebodyWriter{body: buffer, ResponseWriter: c.Writer} c.Writer = blw finish := make(chan struct{}) go func() { // 子协程只会将返回数据写入到内存buff中 c.Next() finish <- struct{}{} }() select { case <-time.After(t): c.Writer.WriteHeader(http.StatusGatewayTimeout) c.Abort() // 1. 主协程超时退出。此时,子协程可能仍在运行 // 如果超时的话,buffer无法主动清除,只能等待GC回收 case <-finish: // 2. 返回结果只会在主协程中被写入 blw.ResponseWriter.Write(buffer.Bytes()) buffpool.PutBuff(buffer) } } } func short(c *gin.Context) { time.Sleep(1 * time.Second) c.JSON(http.StatusOK, gin.H{"hello":"world"}) } func long(c *gin.Context) { time.Sleep(5 * time.Second) c.JSON(http.StatusOK, gin.H{"hello":"world"}) } func main() { // create new gin without any middleware engine := gin.New() // add timeout middleware with 2 second duration engine.Use(Timeout(time.Second * 2)) // create a handler that will last 1 seconds engine.GET("/short", short) // create a route that will last 5 seconds engine.GET("/long", long) // run the server log.Fatal(engine.Run(":8080")) } 简单解释一下: ...

May 16, 2019 · 2 min

玩转CONSUL(2)–分布式锁

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 分布式锁的场景,大家应该都有遇到过。比如对可靠性有较高要求的系统中,我们需要做主备切换。这时我们可以利用分布式锁,来做选主动作,抢到锁作为主,执行对应的任务,剩余的实例作为备份 redis和zookeeper都可以用来做分布式锁,典型的如redis,可以使用SETNX命令来实现分布式锁。本文将介绍基于consul的分布式锁实现 2. 例子 测试例子 test_lock.go package main import ( "github.com/hashicorp/consul/api" "log" "strconv" "sync" "time" ) func main() { wg := &sync.WaitGroup{} for i := 0; i < 3; i++ { wg.Add(1) go tryLock("mylock", "session"+strconv.Itoa(i), wg) } wg.Wait() } func tryLock(key string, sessionName string, wg *sync.WaitGroup) { defer wg.Done() // Get a new client config := &api.Config{ Address: "dev1:8500", Scheme: "http", } client, err := api.NewClient(config) if err != nil { panic(err) } opts := &api.LockOptions{ Key: key, SessionName: sessionName, } lock, err := client.LockOpts(opts) log.Println(sessionName, "try to get lock obj") for i := 0; i < 3; i++ { leaderCh, err := lock.Lock(nil) if err != nil { log.Println("err", err, sessionName) } if leaderCh == nil{ log.Println("err", err, sessionName) continue } log.Println(sessionName, "lock and sleep") time.Sleep(5 * time.Second) err = lock.Unlock() if err != nil { log.Fatal("err", err) } log.Println(sessionName, "unlock") time.Sleep(5 * time.Second) } } 3. 原理 consul中锁的主要是依赖KV Store和Session相关API ...

May 1, 2019 · 2 min

聊聊Protocol Buffers

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 Protocal Buffers是google推出的一种序列化协议。由于它的编码和解码的速度,已经编码后的大小控制的较好,因此它常常被用在RPC调用中,传递参数和结果。比如gRPC。 Protocal Buffers的实现非常简单,本文将对比JSON协议,来聊聊Protocol Buffers的实现以及它高性能的秘密 2. 正篇 2.1 减少传输量(字段名和定界符) 汽车类在Golang中的定义 type Car struct { Age int32 `json:"age"` Color string `json:"color"` Price float32 `json:"price"` } JSON字符串表示 { "age": 10, "color": "red", "price": 15.2568983 } 1)"{" 、"}"、"[", “]"、 双引号、”," 、":" 是为了把字段与字段之间,以及字段的名称和值分隔开。它们不是必须的。 2)字段的名称"age"、“color”、“price"也不是必须的。 如果发送方和接收方都对对象的定义是明晰的,那么字段的名称也不要传递 Protocol Buffers对象定义 message Car { int32 age = 1; string color = 2; double price = 3; } 每个字段都有一个编号,比如在例子中,age是1,color是2,price是3 接收方只要拿到编号,就可以知道需要解析的是哪个字段,它对应的名字甚至是字段值的长度 下图是对Protocol buffers编码的说明 图1 ...

April 3, 2019 · 2 min