玩转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

聊聊关于es打分的有趣现象

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引子 公司内部有简单的搜索引擎,使用ES搭建。 前两天测试人员问我,为什么同一个查询条件,同一条数据,多次查询。score会发生变化。经过验证,确实存在这种问题,那么这种情况到底怎么产生的呢? 2. 例子 来造个例子 2.1 创建index curl -XPUT -H "Content-Type: application/json" dev1:9200/test -d ' { "settings": { "index.number_of_replicas": "2", "index.number_of_shards": "1" }, "mappings": { "_default_": { "dynamic_templates": [], "properties": { "brand": { "type": "keyword" } } } } } 2.2 写入数据 第1次执行 insert1.py import requests for i in range(500): url = "http://dev1:9200/test/car/%d" % (i) res = requests.put(url, json={"brand":"buick", "age":i}) print(i, res.status_code) 第2次执行 insert2.py ...

June 20, 2019 · 2 min

gin的timeout middleware实现(续2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 笔者连续2篇文章,探讨如何开发一个gin的timeout middleware,但是"百密一疏"啊。仍然考虑的不够周全。 笔者的文章 gin的timeout middleware实现(续) 中实现的程序有2个问题 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 // wrap the request context with a timeout ctx, cancel := context.WithTimeout(c.Request.Context(), t) c.Request = c.Request.WithContext(ctx) finish := make(chan struct{}) // 子协程 // ****************注意*************** // 创建的子协程没有recover,存在程序崩溃的风险 go func() { c.Next() finish <- struct{}{} }() // ****************注意*************** select { case <-ctx.Done(): // ****************注意*************** // 子协程和父协程存在同时修改Header的风险 // 由于Header是个map,可能诱发 // fatal error: concurrent map read and map write c.Writer.WriteHeader(http.StatusGatewayTimeout) // ****************注意*************** c.Abort() // 超时发生, 通知子协程退出 cancel() // 如果超时的话,buffer无法主动清除,只能等待GC回收 case <-finish: // 结果只会在主协程中被写入 blw.ResponseWriter.Write(buffer.Bytes()) buffpool.PutBuff(buffer) } } } 2. 解决 main.go ...

June 6, 2019 · 4 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

玩转Prometheus(4)--发现异常节点

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 一个服务常常运行几十或者上百个实例上。通过使用docker容器或者有意的为之,我们会控制实例运行的环境完全一致。因为docker容器所处或者虚拟机是与其他容器或者虚拟机共存的(同一个物理机)。又或者因为物理机的硬件设备的潜在故障,某些实例会表现出异常的行为。(也有可能是程序本身的原因) 如何找到异常节点就变得十分重要。 2. 分析&展示报警 这种节点往往会表现出以下的特点 请求超时 请求错误多 2.1 监控图表 在监控图表上,以非200请求举例,我们可以使用topk列出失败请求最多的实例 topk(3, sum(rate(http_requests_total{project="fake-service", run_mode="product", status!~"200|201|204"}[5m])) by (instance)) 列出HTTP状态码非200的最多的3个实例 图1 从图1我们看出蓝色曲线的实例,非200的HTTP请求数量显著的高于其他实例 2.2 配置报警 参考资料2推荐这样去发现异常实例,如果 某个实例指标的值 > 所有实例指标的平均值 + 2 * 所有实例指标的标准差 那么可能有异常实例存在 DSL floor(max(sum(rate(http_requests_total{project="sdk-api", run_mode="product", status!~"200|201|204"}[5m])) by (instance))) > avg(sum(rate(http_requests_total{project="sdk-api", run_mode="product", status!~"200|201|204"}[5m])) by (instance)) + 2 * stddev(sum(rate(http_requests_total{project="sdk-api", run_mode="product", status!~"200|201|204"}[5m])) by (instance)) 后记 前几天线上就发生了一起这样的故障。找到异常节点,并重启后,故障恢复,报警解出。 参考资料 标准差 Practical Anomaly Detection

April 9, 2019 · 1 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

玩转Prometheus(3)--数据存储

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 在监控系统中,海量的监控数据如何存储,一直是设计人员所必须关心的问题。OpenTSDB选择了Hbase;Open-Falcon选择了RRD(round-robin database)。Prometheus另辟蹊径,借鉴了facebook的paper(参考资料2),设计了自己的TSDB(time series database)。本文试图简单介绍TSDB中使用的2个压缩算法 2. 简单的聊聊TSDB的文件结构 值得一提的是在prometheus中 1)数据是没有做预聚合的,所有的聚合操作都是在查询阶段进行。 据笔者观察查询的时间跨度如果超过7d,速度就会变得比较慢(3 ~ 5秒) 2)Prometheus数据都是单机存储的,数据存在丢失的可能,最近产生的数据存储在内存中,历史数据落在硬盘上,默认数据存储15天。 可以使用storage.tsdb.retention.time来修改数据存储的跨度 --storage.tsdb.retention.time=7d 文件结构 ├── 01D5X2A81S8FMS16S5Q1GWNQDE │ ├── chunks // chunk数据 │ │ └── 000001 │ ├── index // 索引文件 │ ├── meta.json // 人类可读的文件 │ └── tombstones ├── 01D5X2A83TVJD7FFGKPHED5VA1 │ ├── chunks │ │ └── 000001 │ ├── index │ ├── meta.json │ └── tombstones └── wal // wal下的全是预写日志,类似于MySQL中的binlog ├── 00000010 ├── 00000011 ├── 00000012 ├── 00000013 └── checkpoint.000009 └── 00000000 如果读者仔细观察会发现,文件夹的modify时间是递增。 没错,监控数据首先按照时间维度,划分在不同的文件夹中, 然后通过索引文件index去定位不同的series, 实际的数据在chunks文件夹中 。(补充,WAL文件夹存有最近的数据。可简单的把WAL理解为最近的临时数据,chunks中的为归档数据。) ...

March 14, 2019 · 3 min

哈希碰撞攻击与防范机制

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引子 哈希表的原理是用数组来保存键值对,键值对存放的位置(下标)由键的哈希值决定,键的哈希值可以在参数时间内计算出来,这样哈希表插入、查找和删除的时间复杂度为O(1),但是这是理想的情况下,真实的情况是,键的哈希值存在冲突碰撞,也就是不同的键的哈希值可能相等,一个好的哈希函数应该是尽可能的减少碰撞。解决冲突碰撞的方法有分为两种:开放地址法和 链接法,这里不具体展开。哈希表一般都采用链接法来解决冲突碰撞,也就是用一个链表来将分配到同一个桶(键的哈希值一样)的键值对保存起来。 所谓的哈希碰撞攻击就是,针对哈希函数的特性,精心构造数据,使所有数据的哈希值相同,当这些数据保存到哈希表中,哈希表就会退化为单链表,哈希表的各种操作的时间复杂度提升一个数量级,因此会消耗大量CPU资源,导致系统无法快速响应请求,从而达到拒绝服务攻击(Dos)的目的。 绝大多数语言中map的默认实现都是HashMap,比如Golang、Python,Java有2种HashMap和TreeMap 2.攻击方法与防范机制 通过构造哈希值完全相同的字符串 比如 hash(str1) == hash(str2) 找到大量的哈希值相同的str1、str2、str3 … 这种攻击方法依赖的前提是hash函数本身是不变的。 那么如果hash函数是变化的,那么就会大大增加构造哈希值完全相同的字符串的难度。 由此衍生出方案1。 2.1 方案1 加salt 哈希值的计算变为 hash(salt + str) salt为特殊的字符串,一般都是固定值 由于salt是不对外暴露的,所以黑客构造哈希值完全相同的字符串的难度就增大了 2.2 方案2 可变的hash函数 如果hash函数本身就是可变的,那么黑客几乎无法构造哈希值完全相同的字符串。 我们来看Golang是怎么做的 type _type struct { ... kind uint8 alg *typeAlg ... } 每个类型都有其对应的typeAlg // typeAlg is also copied/used in reflect/type.go. // keep them in sync. type typeAlg struct { // function for hashing objects of this type // (ptr to object, seed) -> hash hash func(unsafe.Pointer, uintptr) uintptr // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool } var algarray = [alg_max]typeAlg{ ... alg_STRING: {strhash, strequal}, alg_INTER: {interhash, interequal}, alg_FLOAT32: {f32hash, f32equal}, alg_FLOAT64: {f64hash, f64equal}, alg_CPLX64: {c64hash, c64equal}, ... } func strhash(a unsafe.Pointer, h uintptr) uintptr { x := (*stringStruct)(a) return memhash(x.str, h, uintptr(x.len)) } 最终我们可以看到 ...

March 7, 2019 · 1 min