玩转consul(3)--大规模部署的性能开销定量分析

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 今天有朋友问萌叔,consul能否在大规模生产环境下进行应用。场景是总计大约10w+台机器,分为3 ~ 4个机房,单个机房最多3w万+机器。这个问题大的,可把萌叔吓了跳,部门里面consul集群的规模也就是1k+, 还分好几个机房。 不过他的问题确实也让我十分好奇,consul是否有能力支撑这么规模,我决定针对每个可能性能瓶颈进行定量分析 2. 分析 在进行分析前,我们来看看可能遇到瓶颈有哪些? 下图是consul在多DC情况下的体系架构图 2.1 明确一些概念 consul agent分2种模式server模式和client模式。在每个机房consul server(以下简称为server)会部署3 ~ 5台, 其余的consul节点都是consul client(以下简称为server)。 server的数量不宜过多,否则选主的速度会变慢 数据(包括kv数据,service信息,node信息)都是分机房存储的,由所在机房的server负责。如果需要请求其它机房的数据,则server会将请求转发到对应的机房。 比如dc1的某个应用app1想要获取dc2中key “hello"对应的值 过程如下 app1 --> client --> server(dc1) --> server(dc2) 如果读者仔细观察会发现,consul中,很多api都是可以加上dc参数的 consul kv get hello -datacenter dc2 每个dc的所有server构成一个raft集群,client不参与选主。注意上图的leader和follower标识。 单个机房内部consul节点之间有gossip(端口8301) 机房与机房之间 server节点之间有gossip(端口8302) 2.2 可能的瓶颈 2.2.1 client对server的RPC请求 client使用server的TCP/8300端口发起RPC请求, 管理service、kv都要通过这个端口,它们之间是长连接。 1个机房如果有3w+机器,则client和server至少要建立3w+长连接。 不过萌叔观察了一下,3w+长连接是相对均匀的分布在多个server上的,也就是说如果你有6台consul server, 那么每个server最多处理5k个长连接。还是可以接收的。 对于server的处理能力我简单压测了一下。大约是15k qps Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHz 4核CPU 8G内存 ...

October 25, 2019 · 2 min

玩转NSQ(3)-漂亮的代码实现

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 创建协程 创建协程示例 n.waitGroup.Wrap(func() { // do something }) util.WaitGroupWrapper package util import ( "sync" ) type WaitGroupWrapper struct { sync.WaitGroup } func (w *WaitGroupWrapper) Wrap(cb func()) { w.Add(1) go func() { cb() w.Done() }() } 2. 协程池 用于从Channel扫描数据到Client func (n *NSQD) queueScanLoop() { ... for { select { case <-workTicker.C: if len(channels) == 0 { continue } case <-refreshTicker.C: channels = n.channels() // 协程池中的协程的数量是动态变化的 // 理想数量是与channel的数量保持一致 n.resizePool(len(channels), workCh, responseCh, closeCh) continue case <-n.exitChan: goto exit } ... } // resizePool adjusts the size of the pool of queueScanWorker goroutines // // 1 <= pool <= min(num * 0.25, QueueScanWorkerPoolMax) // func (n *NSQD) resizePool(num int, workCh chan *Channel, responseCh chan bool, closeCh chan int) { idealPoolSize := int(float64(num) * 0.25) if idealPoolSize < 1 { idealPoolSize = 1 } else if idealPoolSize > n.getOpts().QueueScanWorkerPoolMax { idealPoolSize = n.getOpts().QueueScanWorkerPoolMax } for { if idealPoolSize == n.poolSize { break } else if idealPoolSize < n.poolSize { // contract closeCh <- 1 n.poolSize-- } else { // expand n.waitGroup.Wrap(func() { n.queueScanWorker(workCh, responseCh, closeCh) }) n.poolSize++ } } } 3. 通讯协议 nsq中数据被分为2类指令型数据,消息型数据, 2种数据类型,格式不相同 ...

October 9, 2019 · 2 min

玩转NSQ(2)-消息流转

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 nsq的架构简单,代码清晰。对于自主造轮子,实现消息队列、消息推送系统、IM都是非常好的参考。 本文将以图表的形式来说明消息在nsdd中的流转。 2.消息流转 2.1 重要的数据结构 Topic/ Channel/ Client都是nsqd中的数据结构。 数据会从Topic复制到Channel1和Channel2 Client是 consumer在nsqd内部的表征 每个Client最多只能订阅一个Channel 它们内部都有一组queue memoryMsgChan chan *Message // 位于内存 backend BackendQueue //当memoryMsgChan写满,则默认写入磁盘 每个producer都有1个读协程,负责把producer发送的消息写入Topic 多个Client可以订阅同一个Channel。即一个Channel,多个消费者,谁抢到算谁的。 每一个Topic都对应1个协程Topic.messagePump负责从Topic复制数据到Channel NSQD.queueScanLoop会控制一组协程NSQD.queueScanWorker(动态大小的协程池) 从Channel(所有topic的Channels)中复制数据到Client中, 之所以是协程池,我觉得可能跟Channel中的消息有延迟发送,且有重入队列的有关操作 每一个Client对应着一个协程protocolV2.messagePump,负责通过TCP连接把数据发送给consumer 3. 其它有趣的小细节 比较有意思的是,nsq官方推荐,nsqd随着发布者一起部署。 发布者不必去发现其他的nsqd节点,他们总是可以向本地实例发布消息。 实际上解放了producer,而甩锅给了consumer,如果某个Topic 假定叫topic1。如果topic1位于多个nsqd,consumer需要通过nsqlookupd获知所有拥topic1的nsqd的地址,然后需要在多个nsqd上订阅topic1 这里的nsqlookupd相当于是注册中心。 如果某个nsqd宕机,由于nsqd没有副本,消息可能会丢失 打赏作者

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

简易的p2p文件分发系统

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.前言 写了一个非常简单的p2p文件分发系统 主要目的是验证p2p下载的主要思想,所以并不是完全按照BT的公开协议来实现的,已经经过了初步的测试,但并没有在生产环境,进行大规模的使用,因此请谨慎的使用。 传送门 vearne/p2p-sharer 2.组件 2.1 制作种子文件 ./p2p-sharer gen --tracker 192.168.10.200:35330 --filePath /tmp/Motrix-1.4.1.dmg --seedPath ./ tracker tracker 地址 filePath 待分发的文件路径 seedPath 生成的种子文件的存放路径 种子文件示例 { "fileName": "Motrix-1.4.1.dmg", "trackerAddr": "192.168.10.200:35330", "length": 66112269, "pieces": [{ "index": 0, "length": 1048576, "checksum": "92465aae88444d799891ad730877f0f8593f77be" }, { "index": 1, "length": 1048576, "checksum": "7dae558c74d72d67d65533cdcecbf5f6ff5bcce1" }, ... ] } 2.2 tracker 接口文档 保存(供其他node查询)文件分片在整个集群的分布情况 维护node的上下线情况(通过心跳) 2.3 node 接口文档 接收并执行下载任务 与其它node共享文件分片 3. 使用 git clone git@github.com:vearne/p2p-sharer.git 3.1 build 3.1.1 mac env GOOS=darwin GOARCH=amd64 go build ./ 3.1.2 linux env GOOS=linux GOARCH=amd64 go build ./ 3.2 配置文件 请将配置文件放置在 ...

September 10, 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

从状态机看熔断器

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 提到熔断器,大家总会提到Hystrix.但是Hystrix似乎给人一种云里雾里的感觉. 前段时间,我维护的服务也增加了熔断机制,本文中,我将结合我自己的理解来谈谈熔断。 我们选择的是rubyist/circuitbreaker, 这个库的代码可读性比Hystrix以及afex/hystrix-go要高不少,也推荐一下。 2. 关于熔断 如上图所示,熔断器把客户端和服务提供方隔离开来,在客户端调用服务提供方接口时,对服务提供方的服务质量进行监测。如果服务提供方出现问题,则将触发熔断。 这里引出了几个问题 问题1:什么样的情况,可以理解为服务提供方出现了问题? 问题2:触发熔断会怎么样? 问题3:熔断打开以后,如何关闭? 3. 熔断器与状态机 在熔断器内部有3种状态 1. Closed 熔断器关闭 客户端正常访问服务提供方 2. Open 熔断器打开 阻断客户端对服务提供方的访问 3. Half Open 熔断器半开 熔断器开始重新判断是否需要继续熔断 熔断器只是简单在状态机的状态之间切换,至于熔断之后,如何处理客户端的请求,则是更上层业务代码的事情 结合rubyist/circuitbreaker,我来谈谈状态之间的装换条件 3.1 Closed -> Open 服务提供方出现异常 连续发生threshold个错误,立即熔断(ConsecutiveTripFunc) 2)单位时间请求数达到minSamples个,错误率达到rate,即熔断(RateTripFunc) 3)单位时间发生threshold个错误,立即熔断(ThresholdTripFunc) 这里的错误完全是由业务系统来定义,可能是 a) 后端接口的响应严重超时 b) 后端服务返回异常的错误(如HTTP协议 5xx) c) RPC返回有异常的错误码 当熔断器处于Open状态,客户端对服务提供方的访问被阻断了。我们该如何响应客户端的请求? 通常而言,可以有这么几种做法 直接返回给客户端失败信息 将返回降级后的结果 比如针对读请求,可以返回固定值,或者cache中的历史数据 3.2 Open -> Half Open 在熔断一段时间后,服务提供方的服务可能已经恢复了。那么怎么感知到服务提供方的服务已经恢复。 rubyist/circuitbreaker的做法是使用定时器 (2种实现,ExponentialBackOff和ConstantBackOff),每当定时器的设定的时间到达,熔断器的状态从Open 切换到 Half Open ...

July 14, 2019 · 1 min

开发更高质量的服务

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 产品要求的功能都都开发完了,但这并不是终结。怎么样做才能让我们的服务具有更好的质量。 笔者结合自己的遇到的问题和工作中的经验,并以提问的方式,给读者一点点建议 2. 提问 重要性不分先后 2.1 如果服务器重启,服务是否能自动拉起? 2.2 如果程序异常退出,服务是否能够自动拉起? 2.3 如果程序异常退出,能够自动拉起,那么你怎么知道服务是否发生了重启? 2.4 如果是web服务,QPS是多少?每个URL的QPS是多少? 峰值是多少?一般在什么时间点触发? 每种URL的请求量是否合理? 2.5 如果是web服务,每个请求的响应时间是多少?TP90?TP99分别是多少? 2.6 异常请求(比如HTTP非200的比例是多少?)什么样的比例是合理的?为什么? 2.7 如果是多实例部署,那么整个系统的承载极限是多少?如果达到了极限,瓶颈在哪儿(木桶原理中所谓的短板) 2.8 服务都有哪些依赖项(微服务/数据库/文件系统) 其中哪些是无状态的,哪些是弱状态的,哪些是强状态的。这些外部服务和系统,是否已经做到高可用?能否做到快速扩容? 2.9 服务消耗的带宽多少?是否有可能达到带宽上限 如果是面向公网的服务,请求是否已经进行了压缩? 2.10 服务在部署上,是否已经做到了二地三机房 每个地区的网络线路,是否能做到2套或2套以上 (防止光纤被挖断的情况) 2.11 DNS服务是否能够对后端服务进行探活,自动修改域名的解析列表 2.12 部署是否实现了自动化 如果服务需要紧急扩容,该怎么做? 基于docker的容器化方案,还是ansible脚本? 2.13 文档是否完整,文档是否能与代码保持一致 2.14 如果是比较复杂的业务系统,是否有完整的自动化测试脚本 2.15 如果使用了数据库,比如用到了Redis/MySQL Redis连接池和MySQL连接池,是否已经打满(达到最大的poolSize) 打满之后,程序是会等待,还是报错? 2.16 如果使用了Redis,每次Redis操作的耗时是多少?TP90?TP99? 有没有慢查询?这些慢查询是否合理? 2.17 如果使用了MySQL,SQL是否做过审核 哪些表的操作频率最高?那种类型的操作最多? 有没有慢查询?这些慢查询是否合理? 有没有联表过多的查询? 2.18 如果使用了cache,cache的命中率是多少?cache容量是否达到上限? cache会不会与数据库存储不一致,不一致是否可以容许 cache的容量是否达到上限,是否已经触发了LRU 2.19 如果使用了负载均衡,负载均衡与服务之间是长连接吗? 如果服务和其它服务有交互,他们之间是长连接吗? 2.20 服务所在机器CPU负载怎么样?连接数高吗? 是否有大量的TIME_WAIT和CLOSE_WAIT ...

July 2, 2019 · 1 min