深入理解Prometheus(GO SDK及Grafana基本面板)

最近我对Prometheus刮目相看了, 服务加一行代码就能轻轻松松地监控起来服务的CPU使用率、内存、协程数、线程数、打开的文件描述符数量及软限制、重启次数等重要的基本指标, 配合Grafana建立了直观的图表, 对查问题很有帮助, 故想写写折腾Prometheus和Grafana后得到的值得一讲的实践与理解. 介绍 Prometheus是CNCF 的项目之一(ps.CNCF的项目代码都值得研究), 而且还是Graduated Projects. 同时因为其主要是方便灵活的pull方式, 暴露出个http接口出来给prometheusd拉取就行了, 而push方式客户端要做更多的事情, 如果要改push的地址的话就很麻烦, 所以很多著名的项目都在用它, 比如k8s, tidb, etcd, 甚至是时序数据库influxdb都在用它. 我体会到, 很多场景很适合使用Prometheus sdk去加一些指标, 比如logger包, Error级别的消息数是一个很有用的指标; 对于消息队列的SDK, 可以用Prometheus收集客户端侧的发送时延、消费时延、消费处理耗时、消费处理出错等指标; 封装DB操作的SDK, 连接池打开的DB连接数与最大连接数是个很重要的指标; 写个HTTP Middleware, http handler的调用次数、处理时间和responseCode是感兴趣的指标. 安装 Prometheus是Go写的, 故部署方便且跨平台, 一个二进制文件加配置文件就能跑起来. GitHub release页面有各个平台的编译好的二进制文件,通常配合supervisor等进程管理工具来服务化, 也可以用docker. 文档上有基础的配置文件示例, 复制为prometheus.yml即可. ./prometheus --config.file=prometheus.yml prometheus.yml主要是定义一些全局的抓取间隔等参数以及抓取的job, 抓取的job可以指定名字、抓取间隔、抓取目标的IP端口号列表, 目标的路由路径, 额外的label等参数. 抓取指标时会自动加上job="<job_name>"和instance="<target ip port>"两个label, 如果想给job添加额外的固定label, 则可以在配置文件中按如下语法添加. scrape_configs: - job_name: foo metrics_path: "/prometheus/metrics" static_configs: - targets: ['localhost:10056'] labels: service_name: "bar" 服务发现 前面说到, Prometheus的配置文件主要就是定义要抓取的job配置, 显然新加服务要改配置文件是比较麻烦的, Prometheus的一大重要的功能点就是原生支持多种服务发现方式, 支持consul etcd等服务发现组件, 还支持非常通用的基于文件的服务发现, 即你可以定义一个写好target的IP端口号等配置的配置文件路径, 由外部程序定期去更新这个文件, prometheus会定期加载它, 更新抓取的目标, 非常灵活. 数据描述 Prometheus的时序指标数据由timestamp、metric name、label、value组成: timestamp是毫秒级的时间戳. metric name是符合正则[a-zA-Z_:][a-zA-Z0-9_:]*的字符串, 即只包含英文字母和数字及两个特殊符号_:, 不能包含横杆-这样的特殊符号. label是一个kv都是string类型的map. value是float64. 指标类型 Prometheus的指标类型包括基本指标类型Counter和Guage及进阶指标类型Historygram和Summary. 所有指标都是在client SDK端内存存储的, 由prometheus抓取器抓取. Counter Counter是计数器, 单调递增的, 只有服务重启时才会清零, 比如http请求数, errorLevel的log数. 值得一提的是, prometheus的内置函数求值时会自动处理重启清零的情况. counter的value是float64, 怎么无锁地操作float64呢? 答案是用math包将其视作uint64来操作. func (v *value) Add(val float64) { for { oldBits := atomic.LoadUint64(&v.valBits) newBits := math.Float64bits(math.Float64frombits(oldBits) + val) if atomic.CompareAndSwapUint64(&v.valBits, oldBits, newBits) { return } } } Guage Guage是一个可增可减的数值指标, 比如CPU使用率, 内存使用率, 协程数. Historygram Historygram是直方图, 适合需要知道数值分布范围的场景, 比如http请求的响应时长, http请求的响应包体大小等. 直方图的组距不一定是固定的, 可以自己定义适合, 这里称其为bucket, 每一个metric value根据其数值大小落在对应的bucket. Historygram实际上包含多个时序数据. <basename>_bucket{le="<upper inclusive bound>"}小于等于指定数值的计数. <basename>_sum 总和 <basename>_count 总计数, 其值当然也等于<basename>_bucket{le="+Inf"} Summary Summary相比Historygram是按百分位聚合好的直方图, 适合需要知道百分比分布范围的场景, 比如对于 http请求的响应时长, Historygram是侧重在于统计小于1ms的请求有多少个, 1ms~10ms的请求有多少个, 10ms以上的请求有多少个, 而Summary在于统计20%的请求的响应时间是多少, 50%的请求的响应时间是多少, 99%的请求的响应时间是多少. Historygram是计数原始数据, 开销小, 执行查询时有对应的函数计算得到p50, p99, 而Summary是在客户端SDK测做了聚合计算得到指定的百分位, 开销更大一些. SDK的使用 prometheus的Golang SDK设计得很地道, 充分利用了GO语言的特性. 在SDK中所有的指标类型都实现了prometheus.Collector 接口. // Collector is the interface implemented by anything that can be used by // Prometheus to collect metrics. A Collector has to be registered for // collection. See Registerer.Register. // // The stock metrics provided by this package (Gauge, Counter, Summary, // Histogram, Untyped) are also Collectors (which only ever collect one metric, // namely itself). An implementer of Collector may, however, collect multiple // metrics in a coordinated fashion and/or create metrics on the fly. Examples // for collectors already implemented in this library are the metric vectors // (i.e. collection of multiple instances of the same Metric but with different // label values) like GaugeVec or SummaryVec, and the ExpvarCollector. type Collector interface { // Describe sends the super-set of all possible descriptors of metrics // collected by this Collector to the provided channel and returns once // the last descriptor has been sent. The sent descriptors fulfill the // consistency and uniqueness requirements described in the Desc // documentation. (It is valid if one and the same Collector sends // duplicate descriptors. Those duplicates are simply ignored. However, // two different Collectors must not send duplicate descriptors.) This // method idempotently sends the same descriptors throughout the // lifetime of the Collector. If a Collector encounters an error while // executing this method, it must send an invalid descriptor (created // with NewInvalidDesc) to signal the error to the registry. Describe(chan<- *Desc) // Collect is called by the Prometheus registry when collecting // metrics. The implementation sends each collected metric via the // provided channel and returns once the last metric has been sent. The // descriptor of each sent metric is one of those returned by // Describe. Returned metrics that share the same descriptor must differ // in their variable label values. This method may be called // concurrently and must therefore be implemented in a concurrency safe // way. Blocking occurs at the expense of total performance of rendering // all registered metrics. Ideally, Collector implementations support // concurrent readers. Collect(chan<- Metric) } prometheus.Collector 接口中的方法传参都是只写的chan, 使得实现接口的代码无论是同步还是并行都可以. Describe(chan<- *Desc)方法是在将Collector注册或注销时调用的, Collect(chan<- Metric)方法是在被抓取收集指标时调用的. 基本使用 不带label的指标类型使用prometheus.NewCounter prometheus.NewGauge prometheus.NewHistogram prometheus.NewSummary去创建并使用prometheus.MustRegister 注册, 一般是初始化好作为一个包内全局变量, 在init函数中注册. var ( sentBytes = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "etcd", Subsystem: "network", Name: "client_grpc_sent_bytes_total", Help: "The total number of bytes sent to grpc clients.", }) receivedBytes = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "etcd", Subsystem: "network", Name: "client_grpc_received_bytes_total", Help: "The total number of bytes received from grpc clients.", }) ) func init() { prometheus.MustRegister(sentBytes) prometheus.MustRegister(receivedBytes) } counter的Add方法不能传负数, 否则会panic. 带label的指标类型使用prometheus.NewCounterVec prometheus.NewGaugeVec prometheus.NewHistogramVec prometheus.NewSummaryVec, 不同的label值就像空间直角坐标系中的以原点为七点的不同方向的向量一样. 调用Vec类型的WithLabelValues方法传入的value参数数量一定要和注册时定义的label数量一致, 否则会panic. 进阶使用 默认情况下, Collector都是主动去计数, 但有的指标无法主动计数, 比如监控服务当前打开的DB连接数, 这个指标更适合在拉取指标时去获取值, 这个时候就可以使用prometheus.NewCounterFunc prometheus.NewGaugeFunc, 传入一个返回指标值的函数func() float64, 在拉取指标时就会调用这个函数, 当然, 这样定义的是没有带Label的, 如果想在拉取指标时执行自己定义的函数并且附加上label, 就只能自己定义一个实现 prometheus.Collector接口的指标收集器, prometheus SDK设计得足够灵活, 暴露了底层方法MustNewConstMetric, 使得可以很方便地实现一个这样的自定义Collector, 代码如下. type gaugeVecFuncCollector struct { desc *prometheus.Desc gaugeVecFuncWithLabelValues []gaugeVecFuncWithLabelValues labelsDeduplicatedMap map[string]bool } // NewGaugeVecFunc func NewGaugeVecFunc(opts GaugeOpts, labelNames []string) *gaugeVecFuncCollector { return &gaugeVecFuncCollector{ desc: prometheus.NewDesc( prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, labelNames, opts.ConstLabels, ), labelsDeduplicatedMap: make(map[string]bool), } } // Describe func (dc *gaugeVecFuncCollector) Describe(ch chan<- *prometheus.Desc) { ch <- dc.desc } // Collect func (dc *gaugeVecFuncCollector) Collect(ch chan<- prometheus.Metric) { for _, v := range dc.gaugeVecFuncWithLabelValues { ch <- prometheus.MustNewConstMetric(dc.desc, prometheus.GaugeValue, v.gaugeVecFunc(), v.labelValues...) } } // RegisterGaugeVecFunc // 同一组labelValues只能注册一次 func (dc *gaugeVecFuncCollector) RegisterGaugeVecFunc(labelValues []string, gaugeVecFunc func() float64) (err error) { // prometheus每次允许收集一次labelValues相同的metric deduplicateKey := strings.Join(labelValues, "") if dc.labelsDeduplicatedMap[deduplicateKey] { return fmt.Errorf("labelValues func already registered, labelValues:%v", labelValues) } dc.labelsDeduplicatedMap[deduplicateKey] = true handlePanicGaugeVecFunc := func() float64 { if rec := recover(); rec != nil { const size = 10 * 1024 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] logger.Errorf("gaugeVecFunc panic:%v\nstack:%s", rec, buf) } return gaugeVecFunc() } dc.gaugeVecFuncWithLabelValues = append(dc.gaugeVecFuncWithLabelValues, gaugeVecFuncWithLabelValues{ gaugeVecFunc: handlePanicGaugeVecFunc, labelValues: labelValues, }) return nil } 最佳实践 在编辑图表写查询语句时,不会显示指标类型, 所以最好看到metric name就能知道是一个什么类型的指标, 约定counter类型的指标名字以_total为后缀. 在编辑图表写查询语句时, 也不会显示指标类型的单位, 所以最好看到metric name就能知道是一个什么单位的指标, 比如时长要写是纳秒还是毫秒还是秒, http_request_duration_seconds, 数据大小要写是MB还是bytes, client_grpc_sent_bytes_total. 每个指标要有单个词的namespace前缀, 比如process_cpu_seconds_total, http_request_duration_seconds. 不带label的Counter和Guage内部是个无锁的atomic uint64, 不带Label的Historygram内部是多个无锁的atomic uint64, 不带Label的Summary因为内部要聚合计算, 是有锁的, 所以并发要求高的话优先选择Historygram而不是Summary. 带label的每次会去计算label值的hash找到对应的向量, 然后去计数, 所以label数不要太多, label值的长度不要太长, label值是要可枚举的并且不能太多, 否则执行查询时慢, 面板加载慢, 存储也费空间. label如果可以提前计算则尽量使用GetMetricWithLabelValues提前计算好得到一个普通的计数器, 减少每次计数的一次计算label的hash, 提升程序性能. // GetMetricWithLabelValues replaces the method of the same name in // MetricVec. The difference is that this method returns a Counter and not a // Metric so that no type conversion is required. func (m *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) { metric, err := m.MetricVec.GetMetricWithLabelValues(lvs...) if metric != nil { return metric.(Counter), err } return nil, err } 对于时长time.Duration数据类型的指标值收集, time.Since是优化过的, 直接走runtimeNano, 无需走系统调用取当前时间, 性能优于time.Now后相减, 另外, 频繁调用time.Now在性能要求高的程序中也会变成不小的开销. 查询语句promQL Prometheus查询语句(PromQL)是一个相比SQL更简单也很有表达力的专用查询语言, 通过文档及例子学习. Prometheus自带的Graph面板比较简陋, 一般情况下直接用强大的Grafana就行了, 制作图表dashboard时, 直接输入PromQL即可展示时序图表. label条件 (Instant vector selectors) http_requests_total{job="prometheus",group="canary"} 查询条件中,除了=和!=外, =~表示正则匹配, !~表示正则不匹配. 查询条件也可以作用在metric name上, 语法有点像Python的__前缀的魔法, 如用 {__name__=~"job:.*"}表示选择名字符合job:.*这样的正则的metric. 范围条件(Range Vector Selectors) http_requests_total{job="prometheus"}[5m] 范围条件中, 时长字符串语法和GO一样, s代表秒, m代表分, h代表小时, d代表天, w代表星期, y代表年. 常用函数 changes() 变化次数 delta(v range-vector) 平均变化量, 只适用于guage idelta(v range-vector) 即时变化量, 只适用于guage histogram_quantile(φ float, b instant-vector) histogram专用函数, 用来计算p99 p90等百分位的summary. 例子histogram_quantile(0.9, avg(rate(http_request_duration_seconds_bucket[10m])) by (job, le)) increase(v range-vector) 增量, 只适用于counter rate - 平均QPS irate - 即时QPS, 如果原始数据变化快, 可以使用更敏感的irate Snippet 这里列举一些我通过搜索及自行摸索出来的对于Prometheus GO SDK默认收集的指标的PromQL Snippet. CPU使用率: rate(process_cpu_seconds_total[1m])* 100 系统内存使用率: go_memstats_sys_bytes 重启次数: changes(process_start_time_seconds[5m]) Grafana面板 编辑Grafana面板时, 有几个技巧: Query界面可以设置下方说明条Legend的格式, 支持双花括号形式的模板语法, 此处的值在发报警时会作为报警消息内容的一部分. Visualization界面可以设置坐标轴的单位, 比如百分比, 数据大小单位, 时长单位等等, 让Y轴的值更具有可读性. Visualization界面可以设置Legend的更多选项, 是否显示为一个表格, 表格是放在下方还是右方, 支持显示额外的聚合值如最大值最小值平均值当前值总值, 支持设置这些聚合值的小数位数. 监控告警 告警在Grafana处可视化界面设置会比较简单, 可设置连续多少次指定的promQL查出的值不在指定的范围即触发报警, 告警通知的最佳搭配当然是slack channel.

2019/10/6
articleCard.readMore

深入理解ActiveMQ消息队列协议STMOP AMQP MQTT

前言 AWS MQ是完全托管的 ActiveMQ 服务, 最近需要使用, 于是学习其文档, 实践其特性, 由于 ActiveMQ 支持非常丰富的协议, OpenWire amqp stomp mqtt, 所以也学习了各大协议的特性及其SDK. 安装 本地开发最方便的方式当然是docker了, rmohr/activemq 文档比较好的且有aws支持的5.15.6版本的tag. 需要注意的是, 首先要根据其docker hub镜像文档上的几步操作, 将镜像中的默认配置文件复制到自定义的本机conf目录下 /usr/local/activemq/conf, 然后就快速地启动了一个默认配置的 ActiveMQ server # active mq docker run -itd --name activemq \ -p 61616:61616 -p 8161:8161 -p 5672:5672 -p 61613:61613 -p 1883:1883 -p 61614:61614 \ -v /usr/local/activemq/conf:/opt/activemq/conf \ -v /usr/local/activemq/data:/opt/activemq/data \ rmohr/activemq:5.15.6 特性 Advisory ActiveMQ可以将本身的一些事件投递到系统的消息队列, 如 queue/topic的创建, 没有消费者的queue/topic等. http://activemq.apache.org/advisory-message.html 这个特性对于监控MQ非常有用, 默认配置时关闭的, 需要在配置文件activemq.xml中打开. Wildcards 通配符 . 用于分割名字中的多个单词 * 表示任一名字, 不包括点号(.) > 表示任一名字, 包括点号(.), 用于表示前缀, >符号后面不会再跟其他限制条件. 通配符可以用在配置文件中表名作用范围, 也可以用于订阅时的destination名字, 这个功能很不错. Virtual Topic 所谓virtual topic 就是将一个正常的topic, 变成了多个queue. 如TopicA启用了Virtual topic, 则consumer可以去消费 Consumer.xxx.TopicA 这样模式的queue的消息. (http://activemq.apache.org/virtual-destinations.html) xxx对应类似NSQ中的Channel概念. 需要在activemq.xml中配置virtualDestinationInterceptor的范围 prefix及其他选项. name=">" 表示所有的topic都启用virtualTopic功能. prefix="Consumer.*." 表示可以订阅的virtualTopic的pattern是Consumer.. <destinationInterceptors> <virtualDestinationInterceptor> <virtualDestinations> <virtualTopic name=">" prefix="Consumer.*." selectorAware="false"/> </virtualDestinations> </virtualDestinationInterceptor> </destinationInterceptors> Delay & Schedule ActiveMQ支持延时消息及定时消息, 在message header中带上如下字段即可, 其中AMQ_SCHEDULED_PERIOD的最大值是long的最大值, 所以可以设置延时很长时间. Property nametypedescription AMQ_SCHEDULED_DELAYlongThe time in milliseconds that a message will wait before being scheduled to be delivered by the broker AMQ_SCHEDULED_PERIODlongThe time in milliseconds to wait after the start time to wait before scheduling the message again AMQ_SCHEDULED_REPEATintThe number of times to repeat scheduling a message for delivery AMQ_SCHEDULED_CRONStringUse a Cron entry to set the schedule Dead Letter Queue 如果broker投递给消费者消息, 没有ACK或NACK, 则会触发重新投递, 投递超过一定次数则会进入死信队列, 默认只有一个公共的死信队列ActiveMQ.DLQ, 如果需要给topic分别设置死信队列, 则要在修改activemq.xml. <broker>       <destinationPolicy>     <policyMap>       <policyEntries>         <!-- Set the following policy on all queues using the '>' wildcard -->         <policyEntry queue=">">           <deadLetterStrategy>             <!--               Use the prefix 'DLQ.' for the destination name, and make               the DLQ a queue rather than a topic             -->             <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"/>           </deadLetterStrategy>         </policyEntry>       </policyEntries>     </policyMap>   </destinationPolicy> </broker> 默认非持久化的topic不会进入到死信队列中, 如果需要, 则修改activemq.xml, 加入 <!-- Tell the dead letter strategy to also place non-persisted messages onto the dead-letter queue if they can't be delivered. --> <deadLetterStrategy> <... processNonPersistent="true" /> </deadLetterStrategy> 实践 STOMP STOMP是Simple (or Streaming) Text Orientated Messaging Protocol 的缩写, 设计思路借鉴了HTTP, 有content-type, header, body, frame based, text based等类似HTTP的相关概念, 设计文档 < https://stomp.github.io/stomp-specification-1.2.html>, 非常得简洁, 一页就讲完了. 协议细节及特点: 对于重复的header key, 只有第一个有效. 服务端可以限制消息大小, header field数量, header长度. 一个client开多个subscriber时, 必须设置subscribe id. NACK command 表示 requeue. stomp有事务的概念, 消息从producer发出到broker确认收到算一个事务, broker投递到consumer ACK算一个事务, 事务具有原子性. 支持SSL. ActiveMQ作为STOMP server 支持 v1.1版本的STMOP协议. 默认最大消息长度 maxDataLength 为 104857600, maxFrameSize 为 MAX_LONG. 通过 destination 名字前缀是/queue/ 还是 /topic/ 来区分是 queue (生产消费模型)还是 topic(发布订阅模型). 真正的名字是去掉包括两个/符号的前缀后的. 发送默认不是持久化的, 需要在SEND时手动指定persistent:true的header以开启持久化. 订阅默认不是持久化的, 需要在SUBSCRIBE时手动指定activemq.subscriptionName:订阅者名字的header来开启持久化订阅. 很多特性都是靠STOMP header来处理的, ActiveMQ官方文档上有两节讲STOMP的header. http://activemq.apache.org/stomp.html#Stomp-StompExtensionsforJMSMessageSemantics SDK https://github.com/go-stomp/stomp 是目前star数最高的 提了个PR https://github.com/go-stomp/stomp/pull/58 解决了个issue https://github.com/go-stomp/stomp/issues/47 demo 代码 package main import ( "context" "github.com/go-stomp/stomp" "github.com/hanjm/log" "os" "os/signal" "strconv" "sync" "syscall" "time" ) func main() { var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go func() { defer wg.Done() publisher(ctx, "/topic/stomp") }() wg.Add(1) go func() { defer wg.Done() Subscriber(ctx, "channel1", "Consumer.channel1.stomp") }() // wg.Add(1) go func() { defer wg.Done() Subscriber(ctx, "channel2", "Consumer.channel2.stomp") }() wg.Add(1) go func() { defer wg.Done() Subscriber(ctx, "channel3", "/topic/stomp") }() defer func() { cancel() wg.Wait() }() SignalsListen() } func publisher(ctx context.Context, destination string) { conn, err := stomp.Dial("tcp", "127.0.0.1:61613") if err != nil { log.Fatal(err) return } defer conn.Disconnect() for i := 0; ; i++ { select { case <-ctx.Done(): return case <-time.After(time.Second): err = conn.Send( destination, // destination "text/plain", // content-type []byte("Test message #"+strconv.Itoa(i)), stomp.SendOpt.Header("persistent", "true")) // body if err != nil { log.Error(err) return } } } } func Subscriber(ctx context.Context, clientID string, destination string) { conn, err := stomp.Dial("tcp", "127.0.0.1:61613") if err != nil { log.Fatal(err) return } defer conn.Disconnect() sub, err := conn.Subscribe(destination, stomp.AckClientIndividual, stomp.SubscribeOpt.Id(clientID), stomp.SubscribeOpt.Header("persistent", "true")) if err != nil { log.Fatal(err) return } go func() { select { case <-ctx.Done(): err := sub.Unsubscribe() if err != nil { log.Fatal(clientID, err) return } return } }() for m := range sub.C { if m.Err != nil { log.Fatal(err) return } log.Infof("%s msg body:%s", clientID, m.Body) //log.Infof("%s msg header:%s", clientID, *m.Header) //log.Infof("%s msg content-type:%s", clientID, m.ContentType) //log.Infof("%s msg destination:%s", clientID, m.Destination) m.Conn.Ack(m) } log.Info("close sub") } func SignalsListen() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2) switch <-sigs { case syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT: log.Info("service close") } return } MQTT 协议文档http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html 翻译版文档https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/01-Introduction.html 协议细节及特点: transport支持TCP, 也支持WebSocket, 所以定位于IOT. 不支持生产消费模型, 只支持发布订阅模型. 用QOS来表示消息队列中的投递语义, QOS=0 表示至多发送一次, QOS=1表示至少发送一次, QOS=2表示精确地只发送一次. ActiveMQ作为MQTT server 通配符不同, MQTT的 / + # 分别对应 ActiveMQ的. * >. QOS=0对应的是非持久化的topic, QOS=1或者QOS=2对应持久化的topic. AMQP 协议文档: http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-overview-v1.0-os.html AMQP相比 stomp mqtt 就复杂得多, 毕竟名字就是高级消息队列(Advanced Message Queuing Protocol ). 协议细节及特点: AMQP有很多不同的概念, 如Link, Container, Node. 不看模型文档的话就直接使用SDK的话会比较费劲. ContainerID对应ActiveMQ client ID, LinkName对应ActiveMQ subscription name. ActiveMQ作为AMQP server 使用1.0协议, 所以使用了0.9.1的2k star的sdk不能用.(https://github.com/streadway/amqp), 而且官方也认为没必要支持旧版本的协议. 默认最大消息长度 maxDataLength 为 104857600(100MB), maxFrameSize 为 MAX_LONG, consumer持有的未确认最大消息数量prefetch为1000, producerCredit为10000. 可通过连接的URI设定. 支持SSL. 通过 destination 名字前缀是queue:// 还是 topic:// 来区分是 queue (生产消费模型)还是 topic(发布订阅模型). 真正的名字是去掉包括两个/符号的前缀后的. 性能 分别使用 github.com/vcabbage/amqp 76star 13issue 5contributors github.com/go-stomp/stomp 132star 3issue 14contributors github.com/eclipse/paho.mqtt.golang 650star 20issue 34contributors 作为SDK, 分别测试了下pub sub 1KB大小的消息普通场景. publish性能上, amqp=stomp>mqtt, amqp和stomp差不多, 是mqtt的两倍多. subscribe性能上, amqp比stomp快一点, mqtt则慢很多. benchmark代码 package all_bench import ( "bytes" "context" "github.com/eclipse/paho.mqtt.golang" "github.com/go-stomp/stomp" "github.com/hanjm/log" "pack.ag/amqp" "sync/atomic" "testing" "time" ) var msgData = bytes.Repeat([]byte("1"), 1024) var ( stompDestination = "bench-stomp" amqpDestination = "bench-amqp" mqttDestination = "bench-mqtt" pubMsgCount = 20000 subMsgCount = 100 ) func TestMain(m *testing.M) { m.Run() } // go test -bench Publish -benchmem // go test -bench Sub -benchmem func BenchmarkStompPublish(b *testing.B) { conn, err := stomp.Dial("tcp", "127.0.0.1:61613") if err != nil { log.Fatal(err) return } defer conn.Disconnect() b.N = pubMsgCount b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { err = conn.Send( stompDestination, // destination "text/plain", // content-type msgData) // body if err != nil { log.Error(err) return } } } func BenchmarkAmqpPublish(b *testing.B) { // Create client client, err := amqp.Dial("amqp://127.0.0.1", amqp.ConnSASLPlain("system", "manager"), ) if err != nil { log.Fatal("Dialing AMQP server:", err) } defer client.Close() // Open a session session, err := client.NewSession() if err != nil { log.Fatal("Creating AMQP session:", err) } defer func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err = session.Close(ctx) if err != nil { log.Errorf("failed to close session:%s", err) return } //log.Info("session close") }() // Create a sender sender, err := session.NewSender( amqp.LinkTargetAddress(amqpDestination), amqp.LinkSourceDurability(amqp.DurabilityUnsettledState), amqp.LinkSourceExpiryPolicy(amqp.ExpiryNever), ) if err != nil { log.Fatal("Creating sender link:", err) } defer func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := sender.Close(ctx) if err != nil { log.Errorf("failed to close sender:%s", err) return } //log.Infof("sender close") }() ctx := context.Background() msg := amqp.NewMessage(msgData) b.N = pubMsgCount b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { // Send message err = sender.Send(ctx, msg) if err != nil { log.Fatal("Sending message:", err) } if err != nil { log.Fatal(err) return } } } func BenchmarkMqttPublish(b *testing.B) { opt := mqtt.NewClientOptions().SetClientID("pubClient").SetCleanSession(false) opt.AddBroker("tcp://127.0.0.1:1883") client := mqtt.NewClient(opt) t := client.Connect() err := t.Error() if err != nil { log.Fatal(err) return } if t.Wait() { err := t.Error() if err != nil { log.Fatal(err) return } } defer func() { client.Disconnect(10000) }() b.N = pubMsgCount b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { token := client.Publish(mqttDestination, 2, true, msgData) err := token.Error() if err != nil { log.Fatal(err) return } } } func BenchmarkStompSubscriber(b *testing.B) { conn, err := stomp.Dial("tcp", "127.0.0.1:61613") if err != nil { log.Fatal(err) return } clientID := "1" //defer conn.Disconnect() sub, err := conn.Subscribe(stompDestination, stomp.AckClientIndividual, stomp.SubscribeOpt.Id(clientID)) if err != nil { log.Fatal(err) return } //defer func() { //err := sub.Unsubscribe() //if err != nil { //log.Fatal(clientID, err) //return //} //return //}() ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) defer cancel() b.N = subMsgCount b.ReportAllocs() b.ResetTimer() defer b.StopTimer() var i int64 = 0 go func() { for range time.Tick(time.Second) { if atomic.LoadInt64(&i) >= int64(b.N) { cancel() } } }() defer func() { //log.Info("close") }() for { select { case m := <-sub.C: if m.Err != nil { log.Fatal(m.Err) return } m.Conn.Ack(m) i++ if atomic.LoadInt64(&i) > int64(b.N) { return } case <-ctx.Done(): return } } } func BenchmarkAmqpSubscriber(b *testing.B) { // Create client client, err := amqp.Dial("amqp://127.0.0.1", amqp.ConnSASLPlain("system", "manager"), ) if err != nil { log.Fatal("Dialing AMQP server:", err) } //defer client.Close() // Open a session session, err := client.NewSession() if err != nil { log.Fatal("Creating AMQP session:", err) } clientID := "1" defer func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := session.Close(ctx) if err != nil { log.Errorf("%s failed to close session:%s", clientID, err) return } //log.Errorf("%s session close", clientID) }() // Continuously read messages // Create a receiver receiver, err := session.NewReceiver( amqp.LinkSourceAddress(amqpDestination), amqp.LinkCredit(10), ) if err != nil { log.Fatal("Creating receiver link:", err) } defer func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := receiver.Close(ctx) if err != nil { log.Errorf("%s failed to close receiver:%s", clientID, err) return } //log.Errorf("%s receiver close", clientID) }() ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) defer cancel() b.N = subMsgCount b.ReportAllocs() b.ResetTimer() defer b.StopTimer() var i int64 = 0 go func() { for range time.Tick(time.Second) { if atomic.LoadInt64(&i) >= int64(b.N) { cancel() } } }() for { // Receive next message msg, err := receiver.Receive(ctx) if err != nil { if err == context.Canceled { log.Infof("Reading message from AMQP:%s", err) break } log.Errorf("Reading message from AMQP:%s", err) break } //log.Infof("%s msg body:%s value:%T %s", clientID, msg.GetData(), msg.Value, msg.Value) // Accept message msg.Accept() atomic.AddInt64(&i, 1) if atomic.LoadInt64(&i) > int64(b.N) { //log.Info("return") return } } } func BenchmarkMqttSubscriber(b *testing.B) { opt := mqtt.NewClientOptions().SetClientID("subClient").SetCleanSession(false) opt.AddBroker("tcp://127.0.0.1:1883") client := mqtt.NewClient(opt) t := client.Connect() if t.Wait() { err := t.Error() if err != nil { log.Fatal(err) return } } defer func() { client.Disconnect(1000) }() ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) defer cancel() b.N = subMsgCount b.ReportAllocs() b.ResetTimer() defer b.StopTimer() var i int64 = 0 go func() { for range time.Tick(time.Second) { if atomic.LoadInt64(&i) >= int64(b.N) { cancel() } } }() client.Subscribe(mqttDestination, 2, func(c mqtt.Client, m mqtt.Message) { //log.Infof("%s msg body:%s", "1", m.Payload()) m.Ack() atomic.AddInt64(&i, 1) if atomic.LoadInt64(&i) > int64(b.N) { //log.Info("return") return } }) select { case <-ctx.Done(): break } log.Info("close sub") } 一些细节行为 官方的FAQ里面写了一些实现的细节 如果producer比较快而consumer比较慢的话, ActiveMQ的流量控制功能使得producer阻塞. http://activemq.apache.org/what-happens-with-a-fast-producer-and-slow-consumer.html 不支持消费者拿到消息之后Requeue, 即不支持像NSQ那样的消费者出现业务逻辑错误后重试.http://activemq.apache.org/how-do-i-unack-the-message-with-stomp.html. 但是可以利用延时消息实现类似的功能. 性能调优 如果使用了virtualTopic, 那么默认配置下, virtualTopic对应的Queue越多, 发送越慢, 因为默认virtualTopic转发到queue是串行的, 需要调整concurrentSend=true启用并发发送到queue. https://activemq.apache.org/virtual-destinations https://issues.jboss.org/browse/ENTMQ-1093 https://github.com/apache/activemq/blob/9abe2c6f97c92fc99c5a2ef02846f62002a671cf/activemq-broker/src/main/java/org/apache/activemq/broker/region/virtual/VirtualTopicInterceptor.java#L87 concurrentStoreAndDispatchQueues设置为false. 默认配置下, 这个值是true, 根据文档所说在快速消费者情况下, 此值设置为true可以加快持久化消息的性能, 因为被快速消费了消息可以不用落盘, 但实测发现此值为true则10个producer并发发送和1个producer并发发送的性能是一样的没有提高. 设置为false之后提高producer并发则可获得性能倍速提高, 并且单个producer的发送性能并没有下降. 启用mKahaDB, ActiveMQ为了减少打开的文件描述符数量, 默认是用一个KahaDB实例来持久化消息, 但是在磁盘性能比较好的情况下, 一个kahaDB实例发挥不出磁盘的潜力, 启用多个kahaDB后性能可以获得倍速增长. 可以按queue名字的pattern来设置多个kahaDB实例, 也可以使用perDestination="true"设置每个queue一个kahaDB实例, 但这个参数也有坑, 如果destination名字超过了42个字符串, 则会被截断, 发送会报不可恢复的错. 可解决的办法是手动分好destination使用的kahadb, 但是这个配置后续不能动态改了, 只能新开Broker然后迁移. 否则会重启后如果分配规则改变导致分配到了不同的kahadb, 则之前的数据不会被消费. http://sigreen.github.io/2016/02/10/amq-tuning.html https://activemq.apache.org/kahadb#multim-kahadb-persistence-adapter

2019/2/7
articleCard.readMore

Macos Docker container连接宿主机172.17.0.1的办法

在Linux docker container里面, 如果想访问宿主机上的服务, 用 172.17.0.1 这个host即可. 今天在Mac上的 dockercontainer里面启动一个服务, 这个服务需要连我主机上的MySQL, 用 172.17.0.1 是访问不了的, Connection refused. root@d99939cc53fc:/tmp# curl 172.17.0.1:3306 curl: (7) Failed to connect to 172.17.0.1 port 3306: Connection refused 但是看网络结构, 和Linux的一样, 也是在172.17段下的. root@d99939cc53fc:/tmp# ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1 link/ipip 0.0.0.0 brd 0.0.0.0 3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1 link/tunnel6 :: brd :: 6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever 不得其解, Google之, 发现有个隐藏奥秘, https://stackoverflow.com/questions/38504890/docker-for-mac-1-12-0-how-to-connect-to-host-from-container 问题下有人在 Docker Community Edition 17.06.0-ce-mac18, 2017-06-28 的release notes中发现有 Add an experimental DNS name for the host: docker.for.mac.localhost 这样一条更新日志. 页面搜索docker.for.mac.localhost, 发现在 Docker Community Edition 17.12.0-ce-mac46 2018-01-09 的 release notes中发现有一条相关的更新日志 DNS name docker.for.mac.host.internal should be used instead of docker.for.mac.localhost (still valid) for host resolution from containers, since since there is an RFC banning the use of subdomains of localhost. See https://tools.ietf.org/html/draft-west-let-localhost-be-localhost-06. 所以, 结论就是在 container 中应该用 docker.for.mac.host.internal 来访问宿主机. 于是用curl看一下端口通不通, 果然通. root@d99939cc53fc:/tmp# curl docker.for.mac.host.internal:3306 5.7.21Bf

2018/12/16
articleCard.readMore

Nginx With gRPC编译安装

之前写过nginx HTTP2编译安装的文章, 最近想探索下nginx with gRPC support, 所以更新一下. yum apt等包管理系统安装的软件有时候比较旧, 导致一些莫名其妙的问题. 最近在给Nginx加HTTP/2模块中, 编译时加上了--with-http_v2_module参数, 但Chrome请求发现还是不是http2, 后面发现是OpenSSL版本太低. 踩过这一坑后, 感觉Linux下部分软件最好还是自己编译安装比较妥, 如果编译过程出错, 搜下错误信息, 一般是基础依赖没有安装, 很好解决. 官方的源码编译指南 https://nginx.org/en/docs/configure.html https://nginx.org/en/docs/http/ngx_http_v2_module.html (这里写了需要OpenSSL1.0.2以上版本), 很多选项都有合适的默认值, 比如–prefix=/usr/local/nginx, 所以只需要指定自己需要的字段 --user=www-data // 习惯将web相关的服务以www-data用户运行, 如没有此用户可以创建一个也可不加此项按默认nobody用户 --group=www-data --with-http_v2_module // 默认选项不带http2 --with-http_ssl_module // 默认选项不带ssl, 上http2必须要上ssl的 --with-stream // https://nginx.org/en/docs/stream/ngx_stream_core_module.html --with-openssl // 指定OpenSSL --with-pcre=./pcre-8.40 // 需要(version 4.4 — 8.40)的pcre,注意Nginx不支持pcre2 --with-pcre-jit // 打开pcre JIT支持 --with-zlib=./zlib-1.2.11 // 需要(version 1.1.3 — 1.2.11)的zlib以支持gzip 1.官网下载Nginx包 cd /usr/local wget https://nginx.org/download/nginx-1.14.2.tar.gz tar -zxf nginx-1.14.2.tar.gz cd nginx-1.14.2 2.[官网下载OpenSSL 1.0.2以上版本].https://github.com/openssl/openssl/releases cd nginx-1.14.2 wget https://github.com/openssl/openssl/archive/OpenSSL_1_1_0e.tar.gz tar -zxf OpenSSL_1_1_0e.tar.gz 2.官网下载pcre 注意Nginx不支持pcre2,下载pcre最新版即可. 解压到Nginx解压的目录 cd nginx-1.14.2 wget https://ftp.pcre.org/pub/pcre/pcre-8.40.tar.gz tar -zxf pcre-8.40.tar.gz 4.官网下载zlib(version 1.1.3 — 1.2.11) cd nginx-1.14.2 wget http://zlib.net/zlib-1.2.11.tar.gz tar -zxf zlib-1.2.11.tar.gz 5.编译并安装 ./configure \ --user=nginx \ --group=nginx \ --conf-path=/etc/nginx/nginx.conf \ --with-http_v2_module \ --with-http_ssl_module \ --with-stream \ --with-openssl=./openssl-OpenSSL_1_1_0e \ --with-pcre=./pcre-8.40 --with-pcre-jit \ --with-zlib=./zlib-1.2.11 make && make install 6.为了方便操作,软链/usr/local/nginx/sbin/nginx到/usr/local/bin ln -sf /usr/local/nginx/sbin/nginx /usr/local/bin

2018/12/15
articleCard.readMore

Go sql.Driver的mysql Driver 中的一个有意思的行为

如果参数中没有参数, 则直接query. 如果sql中有参数, 且打开了InterpolateParams开关, 那么就客户端直接拼参数到SQL里, 不需要prepare直接query. 如果sql中有参数, 且没有打开InterpolateParams(默认设置), 且带参数, 就会走先prepare再发query参数两步. github.com/go-sql-driver/mysql/connection.go:370 func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) { //... if len(args) != 0 { if !mc.cfg.InterpolateParams { return nil, driver.ErrSkip } // try client-side prepare to reduce roundtrip prepared, err := mc.interpolateParams(query, args) if err != nil { return nil, err } query = prepared } // Send command err := mc.writeCommandPacketStr(comQuery, query) if err == nil { // Read Result // ... 后续再补篇文章详细写写sql.Driver

2018/11/29
articleCard.readMore

学习Influxdb

最近要实现接口监控, 准备用主流的时序数据库influxdb. 基本概念 influxdb的库也 database, 概念和mysql一样 influxdb的表叫 MEASUREMENTS, 意义更贴切, 测量的复数形式. influxdb的一行数据叫 point, 就像做物理实验的打点, 每个点有其值和属性 influxdb的字段分类为 tag 和 field, field就是值, tag是其属性. 拿接口来说, 字段有 service_name, instance_id, method, handler_name, method, request_url, response_code, content_length, response_size, duration. 显然, 前面7个字段是tag, 特点是一般不是数值变量, 可枚举的, 所以influxdb对tag加了索引. 后面3个是field, 是数值变量, 是范围变化的, 不需要加索引. 插入数据 对于插入数据, influxdb同时提供了单条和批量插入的API. 开始不知道有批量方式, 来一条插一条, influxdb CPU巨高. 后面在官网文档找到了办法, 改用批量插入, 大大降低了CPU占用, 官方推荐是5k~1w条数据一批. https://docs.influxdata.com/influxdb/v1.7/concepts/glossary/#batch InfluxData recommends batch sizes of 5,000-10,000 points, although different use cases may be better served by significantly smaller or larger batches. influxdb同时提供了HTTP接口和UDP接口. UDP的好处在于减少了HTTP头部的开销, 性能更好 常用命令 库 # 创建数据库 CREATE DATABASE "db_name" # 显示所有数据库 SHOW DATABASES # 删除数据库 DROP DATABASE "db_name" # 使用数据库 USE mydb 表 # 显示该数据库中的表 SHOW MEASUREMENTS # 创建表, 插入数据时会自动创建 # 删除表 DROP MEASUREMENT "measurementName" 查看数据保留策略 retention polices SHOW RETENTION POLICIES ON "testDB" 创建新的Retention Policies并设置为默认值 # DURATION 保留多少天 # REPLICATION 副本数 CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT 连续查询 # 创建一个连续查询, 每10秒计算一个接口响应耗时平均值到新表 CREATE CONTINUOUS QUERY cq_http_handler_stats_duration ON statsdb BEGIN SELECT mean("duration") INTO http_handler_stats_average_duraion_10s FROM http_handler_stats GROUP BY time(10s) END # 如果成功创建, 那么过了第一个周期后, SHOW MEASUREMENTS 能看到多了一张表 # 显示创建的连续查询 SHOW CONTINUOUS QUERIES # 删除 DROP CONTINUOUS QUERY cq_http_handler_stats_duration ON statsdb 图表 grafana 是 influxdb 的良好伴侣, 写个query语句就能得到很炫酷的图形. 比如接口请求量图表按handler_name, response_code, time(10s) group by就得到了.

2018/11/18
articleCard.readMore

GRPC文档阅读心得

主要是两个文档, grpc repo的文档 https://github.com/grpc/grpc/tree/master/doc , grpc-go repo的文档 https://github.com/grpc/grpc-go/tree/master/Documentation. grpc-go 文档 gRPC Server Reflection Tutorial 在代码中import "google.golang.org/grpc/reflection"包, 然后加一行代码reflection.Register(s), 就可以启用 server reflection. 就可以用grpc_cli去进行获得服务列表, 方法列表, message结构体定义了. reflection.Register(s)实际上是注册了一个特殊的service, 它能列出server中已注册的服务和方法等信息. Compression 用 encoding.RegisterCompressor方法取注册一个压缩器, 启用了压缩的话, 服务端和客户端双方都要进行同样的处理, 服务端在newServer时要带上compressor的serverOption, 客户端在dail的时候要带上WithDefaultCallOptions的DialOption, DialOption加上压缩解压的处理, 不然会得到一个 Internal error, 和HTTP方式一样, 压缩类型体现在content-type的header上. Concurrency Dial得到的ClientConn是并发安全. stream的读写不是并发安全的, sendMsg或RecvMsg不能在多个goroutine中并发地调用,但可以分别在两个goroutine中处理send和Recv. Encoding 序列化反序列化 自定义消息编码解码, 注册一个实现 Codec 接口的对象即可, 然后在Dial或Call时带上grpc.CallContentSubtype这个CallOption, 这样就可以自动处理这个带这个content-type的请求. 默认为 proto 压缩解压缩 自定义压缩解压缩, 注册一个实现 Compressor接口的对象即可, 然后在Dial或Call时带上grpc.UseCompressor这个CallOption. [Mocking Service for gRPC ](https://github.com/grpc/grpc-go/blob/master/Documentation/gomock-example.md) 主要讲如何在单元测试中mock, 用gomock命令行生成实现xx接口的代码, 没什么特别的 [Authentication ](https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-auth-support.md) 主要讲如何进行身份验证, 没什么特别的 Metadata metadata类似HTTP1中的header, 数据结构都是一样的type MD map[string][]string, key都是大小写不敏感的, 但实现规范和HTTP1不一样, HTTP1是按单词之间用连字符”-“分隔, 每个单词第一个字母大写这样的规范来的, 处理起来消耗更大, 而metadata是全转为小写, 实际使用过程中, 提前规范化key能提高不必要的strings.ToLower调用. 用-bin结尾的来传递二进制数据. 服务端handler用metadata.FromIncomingContext(ctx)拿到metadata, 客户端用metadata.AppendToOutgoingContext来附加kv到ctx中. 如果服务端handler又想附加一些信息返回client, 那么就要通过header和trailer传递, 类似responseHeader. func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) { // create and send header header := metadata.Pairs("header-key", "val") grpc.SendHeader(ctx, header) // create and set trailer trailer := metadata.Pairs("trailer-key", "val") grpc.SetTrailer(ctx, trailer) } 然后客户端在调用的时候传要保存的header和trailler的指针到CallOption中, 调用完后指针指向的metadata map就有数据了, 坦率地讲, 我觉得这样处理很麻烦. var header, trailer metadata.MD // variable to store header and trailer r, err := client.SomeRPC( ctx, someRequest, grpc.Header(&header), // will retrieve header grpc.Trailer(&trailer), // will retrieve trailer ) // do something with header and trailer Keepalive gRPC会定时发http2 ping帧来判断连接是否挂掉, 如果ping没有在一定时期内ack, 那么连接会被close. Log Levels grpc-go包默认用gpclog包打日志, grpclog包默认是用多个*log.Logger来实现日志级别, 默认输出到stderr, 对于生产环境, 肯定要集成到自己的日志流里去, 接口是个好东西, grpclog包允许setLog, 实现grpclog.LoggerV2接口即可. info日志包括: grpclog里的info是为了debug DNS 收到了更新 负载均衡器 更新了选择的目标 重要的grpc 状态变更 warn日志包括: warning日志是出现了一些错误, 但还不至于panic. DNS无法解析给定的target 连接server时出错 连接丢失或中断 error日志包括: grpc内部有些error不是用户发起的函数调用, 所以无法返回error给调用者, 只能内部自己打error日志 函数签名没有error, 但调用方传了个错误的参数过来. 内部错误. Fatal日志: fatal日志是出现了不可恢复的内部错误, 要panic. grpc 文档 之前一直有个误区, 多个连接比单个连接要快, 看了 grpc-go issues1、grpc-go issues2 以及 HTTP2文档 才发现, 由于HTTP2有多路复用的特性, 对于同一个sever, 只需要维护一个连接就好了, 没有必要用多个连接去并行复用数据流. 连接数量减少对提升 HTTPS 部署的性能来说是一项特别重要的功能:可以减少开销较大的 TLS 连接数、提升会话重用率,以及从整体上减少所需的客户端和服务器资源。

2018/10/6
articleCard.readMore

Go如何优雅地错误处理(Error Handling and Go 1)

Go的错误处理一直被吐槽太繁琐, 作为主要用GO的攻城狮, 经常写 if err!=nil, 但是如果想偷懒, 少带了上下文信息, 直接写 if err!=nil { return err} 或者 fmt.Errorf 携带的上下文信息太少了的话, 看到错误日志也会一脸懵逼, 难以定位问题. 官方在 2011 年就发过一篇博客教大家如何在Go中处理error https://blog.golang.org/error-handling-and-go , error 是一个内建的 interface, 鼓励大家用好自定义错误类型, 常用的范式有三种: 一是用 errors.New(str string) 定义错误常量, 让调用方去判断返回的 err 是否等于这个常量, 来进行区分处理; 二是用 fmt.Errorf(fmt string, args... interface{}) 增加一些上下文信息, 用文字的方式告诉调用方哪里出错了, 让调用方打错误日志出来; 三是自定义 struct type , 实现 error 接口, 调用方用类型断言转成特定的 struct type , 拿到更结构化的错误信息. 我最开始最常用的做法是, fmt.Errorf 时写上 此函数函数名、调用出错的函数名、参数是什么、err , 代码十分啰嗦, 而且通常打日志是在上层函数打的, 看到错误日志还需要用函数名去代码中搜索看看在哪里出错. 业务代码调用层级一多,非常麻烦. 很多情况下我既想带上下文信息, 又想在上层调用方取得最里层出错的函数返回的error常量或自定义的 struct type, 最好还能自动带上行号函数名信息, 减少每次写 fmt.Errof 的手动写上函数名的痛苦. 于是开始在 github 找包, star 数最高的是 pkg/errors 、juju/errors. pkg/errors 解决了一些问题, 核心函数是 Wrapf 和 Cause: Wrapf包装错误附加上下文信息并带上调用栈, 但是每次去包装错误的时候都去取一次调用栈, 完全没有必要啊, 因为最早出错的函数里就能拿到完整的调用栈的, 并且调用栈打出来的信息也不好看, 而且通常HTTP服务会用框架, 用了框架的话调用栈就会肿起来, 这些框架的固定调用栈信息打印出来毫无帮助. Cause 去递归拿到最里层的 error, 用于和error常量比较或类型断言成自定义 struct type. // Wrapf returns an error annotating err with a stack trace // at the point Wrapf is call, and the format specifier. // If err is nil, Wrapf returns nil. func Wrapf(err error, format string, args ...interface{}) error { if err == nil { return nil } err = &withMessage{ cause: err, msg: fmt.Sprintf(format, args...), } return &withStack{ err, callers(), } } // Cause returns the underlying cause of the error, if possible. // An error value has a cause if it implements the following // interface: // // type causer interface { // Cause() error // } // // If the error does not implement Cause, the original error will // be returned. If the error is nil, nil will be returned without further // investigation. func Cause(err error) error { type causer interface { Cause() error } for err != nil { cause, ok := err.(causer) if !ok { break } err = cause.Cause() } return err } juju/errors API非常复杂, 包装的error的函数就有三个 func Annotatef(other error, format string, args ...interface{}) error 、func Maskf(other error, format string, args ...interface{}) error 、 func Wrapf(other, newDescriptive error, format string, args ...interface{}) error … , 每次包装时都会SetLocation, 消耗更大, 即时有时不需要打印error string 只需要判断, 它也去用runtime.Caller去拿文件名, 行号; 调用栈打出来的信息也不好看. // SetLocation records the source location of the error at callDepth stack // frames above the call. func (e *Err) SetLocation(callDepth int) { _, file, line, _ := runtime.Caller(callDepth + 1) e.file = trimGoPath(file) e.line = line } 以上包不满足要求, 只能造轮子了. 两个思想. API要设计的简单, 调用栈要好看 https://github.com/hanjm/errors API简单: 定义error常量只有 errors.New 函数, 兼容标准库的函数, 兼容很重要; 包装error的只有 errors.Errorf 函数, 只在最早出错的时候取调用栈, 调用方再包装时无需取调用栈, 此时只需要pc, 不需要这时就把文件名行号取出来; 取最里层的 error 只有 errors.GetInnerMost, 用于和 error 常量比较或类型断言成自定义 struct type分类处理. 调用栈好看: 去掉标准包的调用栈, 去掉框架固定的调用栈信息(通常是github.com的包), 只保留业务逻辑的调用栈. 按[ 文件名:行号 函数名:message]分行格式化输出, 把调用栈和附加的message对应起来. (第一版格式是[文件名:行号 函数名:message], 没有空格, 后面有个同事说在Goland IDE里看panic信息时可以点击定位到源码, 你的包能不能加这个功能, 所以去研究了下, 写了几个print的demo试了下发现如果输出中的文件名前后带空格的话, intellij IDE会自动识别输出中的文件名变成超链接, 所以给 “文件名:行号” 前后加了空格, 就能在IDE中直接点击定位到源码对应的行, 非常地方便, 感谢这位同事) 在IDE中加个live template, 写errf回车就补全到 if err!=nil { err = errors.Errorf(err,"{{光标}}") return } 然后补充必要的注释和参数就行了, 在本地环境调试时看到错误日志点击就可以定位到源码, 在非本地环境跑看到错误日志相比之前也能更好地知道发生了什么, 复制文件名:行号到IDE中就能定位到源码, 大大减轻了错误处理的繁琐.

2018/7/8
articleCard.readMore

深入理解NATS & NATS Streaming (踩坑记)

简介 NATS Server是一个高性能的, cloud native的, 基于发布订阅机制的消息系统, 没有消息持久化功能. NATS Streaming Server是基于NATS Server的, 增加消息持久化功能的消息系统. NATS Streaming 持久化特性踩坑记 官网的文档并不详细, 很多重要的技术细节没说, 看了官网的文档之后发现用法很简单, 然后直接去写代码, 写publisher代码没什么问题, 写subscriber代码也能正常工作. 但是subscriber一重启, 重启后重启期间publisher发的消息不会继续收到, 说好的持久化呢? 我把官网的文档翻了遍也没找到答案. 最后在项目的readme.md中找到了答案: 要让subscriber重启后能继续收到重启期间发过来的消息且不重复消息, 必须在调用Subscribe(subject string, cb MsgHandler, opts ...SubscriptionOption) (Subscription, error)订阅时设置一样的durableName, 且重启后连接时Connect(stanClusterID, clientID string, options ...Option) (Conn, error)ClusterID、clientID不能变. 要想理解NATS和NATS Streaming的特性, server和client的readme文档都需要仔细阅读, 特别是nats-streaming服务端的readme. 代码也值得阅读研究. gnatsd服务端 https://github.com/nats-io/gnatsd gnatsd客户端 https://github.com/nats-io/go-nats nats-streaming服务端 https://github.com/nats-io/nats-streaming-server nats-streaming客户端 https://github.com/nats-io/go-nats-streaming 重要特性说明 当subject没有被订阅时, 消息会被直接丢弃, 所以重启订阅者会丢消息, 解决办法: 要么开2个以上客户端实例, 组成队列订阅QueueSubscribe, 要么换NATS Streaming. clientID和durableName对于NATS Streaming非常重要. 要让subscriber重启后能继续收到重启期间发过来的消息且不重复消息, 必须在调用Subscribe(subject string, cb MsgHandler, opts ...SubscriptionOption) (Subscription, error)订阅时设置一样的durableName, 调用Connect(stanClusterID, clientID string, options ...Option) (Conn, error)连接时ClusterID、clientID不能变. 程序关闭时应该使用Close而不是Unsubscribe, Unsubscribe()会删除在server端删除该持久化订阅. This client ID links a given connection to its published messages, subscriptions, especially durable subscriptions. Indeed, durable subscriptions are stored as a combination of the client ID and durable name. If an application wishes to resume message consumption from where it previously stopped, it needs to create a durable subscription. It does so by providing a durable name, which is combined with the client ID provided when the client created its connection. The server then maintain the state for this subscription even after the client connection is closed. Note: The starting position given by the client when restarting a durable subscription is ignored. When the application wants to stop receiving messages on a durable subscription, it should close - but not unsubscribe- this subscription. If a given client library does not have the option to close a subscription, the application should close the connection instead. When the application wants to delete the subscription, it must unsubscribe it. Once unsubscribed, the state is removed and it is then possible to re-use the durable name, but it will be considered a brand new durable subscription, with the start position being the one given by the client when creating the durable subscription. NATS连接时可以设置客户端的名字, 这样在monitor界面中的/connz就能方便地看到各个客户端的统计数据. // Options that can be passed to Connect. // Name is an Option to set the client name. func Name(name string) Option { return func(o *Options) error { o.Name = name return nil } } type ConnInfo struct { Cid uint64 `json:"cid"` IP string `json:"ip"` Port int `json:"port"` Start time.Time `json:"start"` LastActivity time.Time `json:"last_activity"` Uptime string `json:"uptime"` Idle string `json:"idle"` Pending int `json:"pending_bytes"` InMsgs int64 `json:"in_msgs"` OutMsgs int64 `json:"out_msgs"` InBytes int64 `json:"in_bytes"` OutBytes int64 `json:"out_bytes"` NumSubs uint32 `json:"subscriptions"` Name string `json:"name,omitempty"` Lang string `json:"lang,omitempty"` Version string `json:"version,omitempty"` TLSVersion string `json:"tls_version,omitempty"` TLSCipher string `json:"tls_cipher_suite,omitempty"` AuthorizedUser string `json:"authorized_user,omitempty"` Subs []string `json:"subscriptions_list,omitempty"` } 使用.来分隔subject的级别. NATS允许subject包含斜杠/符号, 但NATS Streaming不允许, 因为NATS Streaming持久化时会使用subject名字来作为文件夹名, NATS的subject可以为任意不为空的字符串, 具体的subject不能包含通配符’*’和’>’. NATS Streaming的subject不能为空, 首尾不能为点’.’, 不能包含两个连续的点’.’, 由于暂时不支持通配符订阅功能, 所以不能包含’*’和’>’. NATS Streaming Server实际上是内嵌了一个NATS Server, 自己作为NATS的客户端. NATS Streaming的客户端实际上没有和NATS Streaming Server直接连接, 而是连接内嵌的NATS Server, NATS Streaming Server通过订阅客户端的心跳来知道NATS Streaming客户端连接有没有断开. 所以它强烈建议客户端退出程序时主动Close. NATS可以热重新加载配置, 发送SIGHUP信号或gnatsd -sl reload即可. 开发环境可以加-V参数了解NATS, 生产环境就没必要了, 否则会把发过来的消息全打在日志里. 你甚至可以用NATS的client包publish消息到NATS Streaming, NATS的client可以subscribe, 但NATS Streaming的client无法subscribe, 因为内部的subject变了. 最好不用混用, 容易出问题. NATS Streaming客户端连接时提供的ClusterID和服务端启动配置的ClusterID不一致时会报, 有人表示费解吐槽过, https://github.com/nats-io/nats-streaming-server/issues/309, 但官方解释说没有问题, Timeout也说的通. If you provide a cluster ID not used by any of the servers in the network, no server will respond to the client, hence the timeout error message from the client library. If anything, this is an error message that needs to be updated in the client libraries, not in the server. ChanSubscribe方式的客户端优雅关闭, 等待消息处理完成. package main import ( "fmt" "os" "syscall" "os/signal" "github.com/nats-io/go-nats" "sync" ) func main() { n, err := nats.Connect("nats://127.0.0.1:7222", nats.Name("test_client"), nats.UserInfo("", "")) if err != nil { panic(err) } subject := "test" msgCh := make(chan *nats.Msg, nats.DefaultMaxChanLen) _, err = n.ChanSubscribe(subject, msgCh) if err != nil { panic(err) } wg := sync.WaitGroup{} for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() // msg handler for msg := range msgCh { fmt.Printf("%s\n", msg.Data) } }() } quit := make(chan os.Signal) signal.Notify(quit, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2) select { case <-quit: defer wg.Wait() // close msgCh and wait process ok close(msgCh) n.Flush() n.Close() } } NATS代码中的技巧 很有用的Go风格的可选参数设计模式, 很多地方见过. // Option is a function on the options for a connection. type Option func(*Options) error // Options can be used to create a customized connection. type Options struct { Url string ... User string Password string } var DefaultOptions = Options{ AllowReconnect: true, MaxReconnect: DefaultMaxReconnect, ReconnectWait: DefaultReconnectWait, Timeout: DefaultTimeout, PingInterval: DefaultPingInterval, MaxPingsOut: DefaultMaxPingOut, SubChanLen: DefaultMaxChanLen, ReconnectBufSize: DefaultReconnectBufSize, Dialer: &net.Dialer{ Timeout: DefaultTimeout, }, } // Connect will attempt to connect to the NATS system. // The url can contain username/password semantics. e.g. nats://derek:pass@localhost:4222 // Comma separated arrays are also supported, e.g. urlA, urlB. // Options start with the defaults but can be overridden. func Connect(url string, options ...Option) (*Conn, error) { opts := DefaultOptions opts.Servers = processUrlString(url) for _, opt := range options { if err := opt(&opts); err != nil { return nil, err } } return opts.Connect() } // Options that can be passed to Connect. // Name is an Option to set the client name. func Name(name string) Option { return func(o *Options) error { o.Name = name return nil } } 使用ringBuffer限制消息数量 You can view a message log as a ring buffer. Messages are appended to the end of the log. If a limit is set globally for all channels, or specifically for this channel, when the limit is reached, older messages are removed to make room for the new ones. 用reflect来绑定任意类型的chan chVal := reflect.ValueOf(channel) if chVal.Kind() != reflect.Chan { return ErrChanArg } val, ok := chVal.Recv() if !ok { // Channel has most likely been closed. return }

2018/2/17
articleCard.readMore

深入理解GO时间处理(time.Time)

1. 前言 时间包括时间值和时区, 没有包含时区信息的时间是不完整的、有歧义的. 和外界传递或解析时间数据时, 应当像HTTP协议或unix-timestamp那样, 使用没有时区歧义的格式, 如果使用某些没有包含时区的非标准的时间表示格式(如yyyy-mm-dd HH:MM:SS), 是有隐患的, 因为解析时会使用场景的默认设置, 如系统时区, 数据库默认时区可能引发事故. 确保服务器系统、数据库、应用程序使用统一的时区, 如果因为一些历史原因, 应用程序各自保持着不同时区, 那么编程时要小心检查代码, 知道时间数据在使用不同时区的程序之间交换时的行为. 第三节会详细解释go程序在不同场景下time.Time的行为. 2. Time的数据结构 go1.9之前, time.Time的定义为 type Time struct { // sec gives the number of seconds elapsed since // January 1, year 1 00:00:00 UTC. sec int64 // nsec specifies a non-negative nanosecond // offset within the second named by Seconds. // It must be in the range [0, 999999999]. nsec int32 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location } sec表示从公元1年1月1日00:00:00UTC到要表示的整数秒数, nsec表示余下的纳秒数, loc表示时区. sec和nsec处理没有歧义的时间值, loc处理偏移量. 因为2017年闰一秒, 国际时钟调整, Go程序两次取time.Now()相减的时间差得到了意料之外的负数, 导致cloudFlare的CDN服务中断, 详见https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/, go1.9在不影响已有应用代码的情况下修改了time.Time的实现. go1.9的time.Time定义为 // A Time represents an instant in time with nanosecond precision. // // Programs using times should typically store and pass them as values, // not pointers. That is, time variables and struct fields should be of // type time.Time, not *time.Time. // // A Time value can be used by multiple goroutines simultaneously except // that the methods GobDecode, UnmarshalBinary, UnmarshalJSON and // UnmarshalText are not concurrency-safe. // // Time instants can be compared using the Before, After, and Equal methods. // The Sub method subtracts two instants, producing a Duration. // The Add method adds a Time and a Duration, producing a Time. // // The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC. // As this time is unlikely to come up in practice, the IsZero method gives // a simple way of detecting a time that has not been initialized explicitly. // // Each Time has associated with it a Location, consulted when computing the // presentation form of the time, such as in the Format, Hour, and Year methods. // The methods Local, UTC, and In return a Time with a specific location. // Changing the location in this way changes only the presentation; it does not // change the instant in time being denoted and therefore does not affect the // computations described in earlier paragraphs. // // Note that the Go == operator compares not just the time instant but also the // Location and the monotonic clock reading. Therefore, Time values should not // be used as map or database keys without first guaranteeing that the // identical Location has been set for all values, which can be achieved // through use of the UTC or Local method, and that the monotonic clock reading // has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u) // to t == u, since t.Equal uses the most accurate comparison available and // correctly handles the case when only one of its arguments has a monotonic // clock reading. // // In addition to the required “wall clock” reading, a Time may contain an optional // reading of the current process's monotonic clock, to provide additional precision // for comparison or subtraction. // See the “Monotonic Clocks” section in the package documentation for details. // type Time struct { // wall and ext encode the wall time seconds, wall time nanoseconds, // and optional monotonic clock reading in nanoseconds. // // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. // The nanoseconds field is in the range [0, 999999999]. // If the hasMonotonic bit is 0, then the 33-bit field must be zero // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit // unsigned wall seconds since Jan 1 year 1885, and ext holds a // signed 64-bit monotonic clock reading, nanoseconds since process start. wall uint64 ext int64 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location } 3. time的行为 构造时间-获取现在时间-time.Now(), time.Now()使用本地时间, time.Local即本地时区, 取决于运行的系统环境设置, 优先取”TZ”这个环境变量, 然后取/etc/localtime, 都取不到就用UTC兜底. func Now() Time { sec, nsec := now() return Time{sec + unixToInternal, nsec, Local} } 构造时间-获取某一时区的现在时间-time.Now().In(), Time结构体的In()方法仅设置loc, 不会改变时间值. 特别地, 如果是获取现在的UTC时间, 可以使用Time.Now().UTC(). 时区不能为nil. time包中只有两个时区变量time.Local和time.UTC. 其他时区变量有两种方法取得, 一个是通过time.LoadLocation函数根据时区名字加载, 时区名字见IANA Time Zone database, LoadLocation首先查找系统zoneinfo, 然后查找$GOROOT/lib/time/zoneinfo.zip.另一个是在知道时区名字和偏移量的情况下直接调用time.FixedZone("$zonename", $offsetSecond)构造一个Location对象. // In returns t with the location information set to loc. // // In panics if loc is nil. func (t Time) In(loc *Location) Time { if loc == nil { panic("time: missing Location in call to Time.In") } t.setLoc(loc) return t } // LoadLocation returns the Location with the given name. // // If the name is "" or "UTC", LoadLocation returns UTC. // If the name is "Local", LoadLocation returns Local. // // Otherwise, the name is taken to be a location name corresponding to a file // in the IANA Time Zone database, such as "America/New_York". // // The time zone database needed by LoadLocation may not be // present on all systems, especially non-Unix systems. // LoadLocation looks in the directory or uncompressed zip file // named by the ZONEINFO environment variable, if any, then looks in // known installation locations on Unix systems, // and finally looks in $GOROOT/lib/time/zoneinfo.zip. func LoadLocation(name string) (*Location, error) { if name == "" || name == "UTC" { return UTC, nil } if name == "Local" { return Local, nil } if zoneinfo != "" { if z, err := loadZoneFile(zoneinfo, name); err == nil { z.name = name return z, nil } } return loadLocation(name) } 构造时间-手动构造时间-time.Date(), 传入年元日时分秒纳秒和时区变量Location构造一个时间. 得到的是指定location的时间. func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time { if loc == nil { panic("time: missing Location in call to Date") } ..... } 构造时间-从unix时间戳中构造时间, time.Unix(), 传入秒和纳秒构造. 序列化反序列化时间-文本和JSON, fmt.Sprintf,fmt.SScanf, json.Marshal, json.Unmarshal时的, 使用的时间格式均包含时区信息, 序列化使用RFC3339Nano()”2006-01-02T15:04:05.999999999Z07:00”, 反序列化使用RFC3339()”2006-01-02T15:04:05Z07:00”, 反序列化没有纳秒值也可以正常序列化成功. // String returns the time formatted using the format string //"2006-01-02 15:04:05.999999999 -0700 MST" func (t Time) String() string { return t.Format("2006-01-02 15:04:05.999999999 -0700 MST") } // MarshalJSON implements the json.Marshaler interface. // The time is a quoted string in RFC 3339 format, with sub-second precision added if present. func (t Time) MarshalJSON() ([]byte, error) { if y := t.Year(); y < 0 || y >= 10000 { // RFC 3339 is clear that years are 4 digits exactly. // See golang.org/issue/4556#c15 for more discussion. return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") } b := make([]byte, 0, len(RFC3339Nano)+2) b = append(b, '"') b = t.AppendFormat(b, RFC3339Nano) b = append(b, '"') return b, nil } // UnmarshalJSON implements the json.Unmarshaler interface. // The time is expected to be a quoted string in RFC 3339 format. func (t *Time) UnmarshalJSON(data []byte) error { // Ignore null, like in the main JSON package. if string(data) == "null" { return nil } // Fractional seconds are handled implicitly by Parse. var err error *t, err = Parse(`"`+RFC3339+`"`, string(data)) return err } 序列化反序列化时间-HTTP协议中的date, 统一GMT, 代码位于net/http/server.go:878 // TimeFormat is the time format to use when generating times in HTTP // headers. It is like time.RFC1123 but hard-codes GMT as the time // zone. The time being formatted must be in UTC for Format to // generate the correct format. // // For parsing this time format, see ParseTime. const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" 序列化反序列化时间-time.Format("$layout"), time.Parse("$layout","$value"), time.ParseInLocation("$layout","$value","$Location") time.Format("$layout")格式化时间时, 时区会参与计算. 调time.Time的Year()Month()Day()等获取年月日等时时区会参与计算, 得到一个使用偏移量修正过的正确的时间字符串, 若$layout有指定显示时区, 那么时区信息会体现在格式化后的时间字符串中. 如果$layout没有指定显示时区, 那么字符串只有时间没有时区, 时区是隐含的, time.Time对象中的时区. time.Parse("$layout","$value"), 若$layout有指定显示时区, 那么时区信息会体现在格式化后的time.Time对象. 如果$layout没有指定显示时区, 那么使用会认为这是一个UTC时间, 时区是UTC. time.ParseInLocation("$layout","$value","$Location") 使用传参的时区解析时间, 建议用这个, 没有歧义. // Parse parses a formatted string and returns the time value it represents. // The layout defines the format by showing how the reference time, // defined to be //Mon Jan 2 15:04:05 -0700 MST 2006 // would be interpreted if it were the value; it serves as an example of // the input format. The same interpretation will then be made to the // input string. // // Predefined layouts ANSIC, UnixDate, RFC3339 and others describe standard // and convenient representations of the reference time. For more information // about the formats and the definition of the reference time, see the // documentation for ANSIC and the other constants defined by this package. // Also, the executable example for time.Format demonstrates the working // of the layout string in detail and is a good reference. // // Elements omitted from the value are assumed to be zero or, when // zero is impossible, one, so parsing "3:04pm" returns the time // corresponding to Jan 1, year 0, 15:04:00 UTC (note that because the year is // 0, this time is before the zero Time). // Years must be in the range 0000..9999. The day of the week is checked // for syntax but it is otherwise ignored. // // In the absence of a time zone indicator, Parse returns a time in UTC. // // When parsing a time with a zone offset like -0700, if the offset corresponds // to a time zone used by the current location (Local), then Parse uses that // location and zone in the returned time. Otherwise it records the time as // being in a fabricated location with time fixed at the given zone offset. // // No checking is done that the day of the month is within the month's // valid dates; any one- or two-digit value is accepted. For example // February 31 and even February 99 are valid dates, specifying dates // in March and May. This behavior is consistent with time.Date. // // When parsing a time with a zone abbreviation like MST, if the zone abbreviation // has a defined offset in the current location, then that offset is used. // The zone abbreviation "UTC" is recognized as UTC regardless of location. // If the zone abbreviation is unknown, Parse records the time as being // in a fabricated location with the given zone abbreviation and a zero offset. // This choice means that such a time can be parsed and reformatted with the // same layout losslessly, but the exact instant used in the representation will // differ by the actual zone offset. To avoid such problems, prefer time layouts // that use a numeric zone offset, or use ParseInLocation. func Parse(layout, value string) (Time, error) { return parse(layout, value, UTC, Local) } // ParseInLocation is like Parse but differs in two important ways. // First, in the absence of time zone information, Parse interprets a time as UTC; // ParseInLocation interprets the time as in the given location. // Second, when given a zone offset or abbreviation, Parse tries to match it // against the Local location; ParseInLocation uses the given location. func ParseInLocation(layout, value string, loc *Location) (Time, error) { return parse(layout, value, loc, loc) } func parse(layout, value string, defaultLocation, local *Location) (Time, error) { ..... } 序列化反序列化时间-go-sql-driver/mysql中的时间处理. MySQL驱动解析时间的前提是连接字符串加了parseTime和loc, 如果parseTime为false, 会把mysql的date类型变成[]byte/string自行处理, parseTime为true才处理时间, loc指定MySQL中存储时间数据的时区, 如果没有指定loc, 用UTC. 序列化和反序列化均使用连接字符串中的设定的loc, SQL语句中的time.Time类型的参数的时区信息如果和loc不同, 则会调用t.In(loc)方法转时区. 解析连接字符串的代码位于parseDSNParams函数https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L467-L490 // Time Location case "loc": if value, err = url.QueryUnescape(value); err != nil { return } cfg.Loc, err = time.LoadLocation(value) if err != nil { return } // time.Time parsing case "parseTime": var isBool bool cfg.ParseTime, isBool = readBool(value) if !isBool { return errors.New("invalid bool value: " + value) } 解析SQL语句中time.Time类型的参数的代码位于mysqlConn.interpolateParams方法https://github.com/go-sql-driver/mysql/blob/master/connection.go#L230-L273 case time.Time: if v.IsZero() { buf = append(buf, "'0000-00-00'"...) } else { v := v.In(mc.cfg.Loc) v = v.Add(time.Nanosecond * 500) // To round under microsecond year := v.Year() year100 := year / 100 year1 := year % 100 month := v.Month() day := v.Day() hour := v.Hour() minute := v.Minute() second := v.Second() micro := v.Nanosecond() / 1000 buf = append(buf, []byte{ '\'', digits10[year100], digits01[year100], digits10[year1], digits01[year1], '-', digits10[month], digits01[month], '-', digits10[day], digits01[day], ' ', digits10[hour], digits01[hour], ':', digits10[minute], digits01[minute], ':', digits10[second], digits01[second], }...) if micro != 0 { micro10000 := micro / 10000 micro100 := micro / 100 % 100 micro1 := micro % 100 buf = append(buf, []byte{ '.', digits10[micro10000], digits01[micro10000], digits10[micro100], digits01[micro100], digits10[micro1], digits01[micro1], }...) } buf = append(buf, '\'') } 从MySQL数据流中解析时间的代码位于textRows.readRow方法https://github.com/go-sql-driver/mysql/blob/master/packets.go#L772-L777, 注意只要MySQL连接字符串设置了parseTime=true, 就会解析时间, 不管你是用string还是time.Time接收的. if !isNull { if !mc.parseTime { continue } else { switch rows.rs.columns[i].fieldType { case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeDate, fieldTypeNewDate: dest[i], err = parseDateTime( string(dest[i].([]byte)), mc.cfg.Loc, ) if err == nil { continue } default: continue } } } 4. time时区处理不当案例 有个服务频繁使用最新汇率, 所以缓存了最新汇率对象, 汇率对象的过期时间设为第二天北京时间零点, 汇率过期则从数据库中去最新汇率, 设置过期时间的代码如下: var startTime string = time.Now().UTC().Add(8 * time.Hour).Format("2006-01-02") tm2, _ := time.Parse("2006-01-02", startTime) lastTime = tm2.Unix() + 24*60*60 这段代码使用了time.Parse, 如果时间格式中没有指定时区, 那么会得到使用本地时区下的第二天零点, 服务器时区设置为UTC0, 于是汇率缓存在UTC零点即北京时间八点才更新. 公共库中有一个GetBjTime()方法, 注释写着将服务器UTC转成北京时间, 代码如下 // 原版 func GetBjTime() time.Time { // 将服务器UTC转成北京时间 uTime := time.Now().UTC() dur, _ := time.ParseDuration("+8h") return uTime.Add(dur) } // 改 func GetBjTime() time.Time { // 将服务器UTC转成北京时间 uTime := time.Now() return uTime.In(time.FixedZone("CST", 8*60*60)) } 同事用这个方法将得到的time.Time参与计算, 发现多了8个小时. 觉得有问题, 同事和我讨论了之后, 我们得出结论后就大意地直接把原有函数改了, 我们都没有意识到这是个非常危险操作, 只所以危险是因为这个函数已经在很多服务的代码里用着(要稳!不能乱动公共库!!!). 之前用这个函数是因为老Java项目运行在时区为东八区的系统上, 大量代码使用东八区时间, 但数据库MySQL时区设置为UTC, go项目也运行在UTC时区. 也就是说, Java项目在把时区为UTC数据库当做是东八区来用, Java程序往MySQL写东八区的时间字符串, 在sequel软件中看表内容时虽然字符串是一样的, 但其实内部是UTC的时间, go代码的mysql连接字符串中loc选项为空, 就会使用UTC时区去解析数据, 拿到的数据会多八个小时. 例如Java代码往mysql插入一条”2017-10-29 22:00:00”数据本意是东八区2017年10月29日22点, 但在MySQL内部看来, 这是UTC的2017年10月29日22点, 换算成东八区时间为2017年10月30日6点, 如果其它程序解析时认为时间数据是MySQL的UTC时区, 那么会得到一个错误的时间. 所以才会在GO中要往Java代码创建的表写入数据时用time.Now().UTC().Add(time.Hour*8)直接相加八小时使得Java项目行为一致, 拿UTC的数据库存东八区时间. 后面想想, 面对这种数据库中有时区不一致数据的情况, 在没有办法统一UTC时区的情况下, 应当使用MySQL时间字符串而不是time.Time来传递以避免时区隐含转换问题, 写入时参数传string类型的时间字符串, 解析时先拿到时间字符串, 然后自行判断建表时这个字段用的是东八区的时间字符串还是UTC时间字符串进行time.ParseInLocation得到时间对象, MySQL连接字符串的parseTime选项要设置为false. 比如我想在MySQL中存东八区的当前时间, SQL参数用Format后的字符串而不是传time.Time, 原版的time.Now().UTC().Add(time.Hour*8).Format("2006-01-02 15:04:05")和修改的time.Now().In(time.FixedZone("CST", 8*60*60))的输出将是一样, 但后者是正确的东八区现在时间. 原版的GetBjTime()返回time.Time可能用GetBeijingNowTimeString返回string更能体现本意吧. 5. 时间有关的标准 UTC 协调世界时(英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间。中华民国采用CNS 7648的《资料元及交换格式–资讯交换–日期及时间的表示法》(与ISO 8601类似)称之为世界协调时间。中华人民共和国采用ISO 8601:2000的国家标准GB/T 7408-2005《数据元和交换格式 信息交换 日期和时间表示法》中亦称之为协调世界时。 协调世界时是世界上调节时钟和时间的主要时间标准,它与0度经线的平太阳时相差不超过1秒[4],并不遵守夏令时。协调世界时是最接近格林威治标准时间(GMT)的几个替代时间系统之一。对于大多数用途来说,UTC时间被认为能与GMT时间互换,但GMT时间已不再被科学界所确定。 ISO 8601 计算某一天在一年的第几周/循环时间RRlue/会用到此标准 国际标准ISO 8601,是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前是第三版“ISO8601:2004”以替代第一版“ISO8601:1988”与第二版“ISO8601:2000”。 UNIX时间 UNIX时间,或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从协调世界时1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒[1]。 在多数Unix系统上Unix时间可以通过date +%s指令来检查。 时区 时区列表

2017/10/29
articleCard.readMore

Go如何精确计算小数-Decimal研究-Tidb MyDecimal问题

1 浮点数为什么不精确 先看两个case // case1: 135.90*100 ==== // float32 var f1 float32 = 135.90 fmt.Println(f1 * 100) // output:13589.999 // float64 var f2 float64 = 135.90 fmt.Println(f2 * 100) // output:13590 浮点数在单精度下, 135.9*100即出现了偏差, 双精度下结果正确. // case2: 0.1 add 10 times === // float32 var f3 float32 = 0 for i := 0; i < 10; i++ { f3 += 0.1 } fmt.Println(f3) //output:1.0000001 // float64 var f4 float64 = 0 for i := 0; i < 10; i++ { f4 += 0.1 } fmt.Println(f4) //output:0.9999999999999999 0.1加10次, 这下无论是float32和float64都出现了偏差. 为什么呢, Go和大多数语言一样, 使用标准的IEEE754表示浮点数, 0.1使用二进制表示结果是一个无限循环数, 只能舍入后表示, 累加10次之后就会出现偏差. 此外, 还有几个隐藏的坑https://play.golang.org/p/bQPbirROmN float32和float64直接互转会精度丢失, 四舍五入后错误. int64转float64在数值很大的时候出现偏差. 合理但须注意: 两位小数乘100强转int, 比期望值少了1. package main import ( "fmt" ) func main() { // case: float32==>float64 // 从数据库中取出80.45, 历史代码用float32接收 var a float32 = 80.45 var b float64 // 有些函数只能接收float64, 只能强转 b = float64(a) // 打印出值, 强转后出现偏差 fmt.Println(a) //output:80.45 fmt.Println(b) //output:80.44999694824219 // ... 四舍五入保留小数点后1位, 期望80.5, 结果是80.4 // case: int64==>float64 var c int64 = 987654321098765432 fmt.Printf("%.f\n", float64(c)) //output:987654321098765440 // case: int(float64(xx.xx*100)) var d float64 = 1129.6 var e int64 = int64(d * 100) fmt.Println(e) //output:112959 } ##2 数据库是怎么做的 MySQL提供了decimal(p,d)/numberlic(p,d)类型的定点数表示法, 由p位数字(不包括符号、小数点)组成, 小数点后面有d位数字, 占p+2个字节, 计算性能会比double/float类型弱一些. ##3 Go代码如何实现Decimal Java有成熟的标准库java.lang.BigDecimal,Python有标准库Decimal, 可惜GO没有. 在GitHub搜decimal, star数量比较多的是TiDB里的MyDecimal和ithub.com/shopspring/decimal的实现. shopspring的Decimal实现比较简单, 思路是使用十进制定点数表示法, 有多少位小数就小数点后移多少位, value保存移之后的整数, exp保存小数点后的数位个数, number=value*10^exp, 因为移小数点后的整数可能很大, 所以这里借用标准包里的math/big表示这个大整数. exp使用了int32, 所以这个包最多能表示小数点后有32个十进制数位的情况. Decimal结构体的定义如下 // Decimal represents a fixed-point decimal. It is immutable. // number = value * 10 ^ exp type Decimal struct { value *big.Int // NOTE(vadim): this must be an int32, because we cast it to float64 during // calculations. If exp is 64 bit, we might lose precision. // If we cared about being able to represent every possible decimal, we // could make exp a *big.Int but it would hurt performance and numbers // like that are unrealistic. exp int32 } TiDB里的MyDecimal定义位于github.com/pingcap/tidb/util/types/mydecimal.go, 实现比shopspring的Decimal复杂多了, 也更底层(不依赖math/big), 性能也更好(见下面的benchmark). 其思路是: digitsInt保存数字的整数部分数字个数, digitsFrac保存数字的小数部分数字个数, resultFrac保存计算及序列化时保留至小数点后几位, negative标明数字是否为负数, wordBuf是一个定长的int32数组(长度为9), 数字去掉小数点的主体保存在这里, 一个int32有32个bit, 最大值为(2**31-1)2147483647(10个十进制数), 所以一个int32最多能表示9个十进制数位, 因此wordBuf 最多能容纳9*9个十进制数位. // MyDecimal represents a decimal value. type MyDecimal struct { digitsInt int8 // the number of *decimal* digits before the point. digitsFrac int8 // the number of decimal digits after the point. resultFrac int8 // result fraction digits. negative bool // wordBuf is an array of int32 words. // A word is an int32 value can hold 9 digits.(0 <= word < wordBase) wordBuf [maxWordBufLen]int32 } 看看这两种decimal类型在文首的两个case下的结果, 同时跑个分. main_test.go package main import ( "testing" "github.com/shopspring/decimal" "github.com/pingcap/tidb/util/types" "log" ) var case1String = "135.90" var case1Bytes = []byte(case1String) var case2String = "0" var case2Bytes = []byte("0") func ShopspringDecimalCase1() decimal.Decimal { dec1, err := decimal.NewFromString(case1String) if err != nil { log.Fatal(err) } dec2 := decimal.NewFromFloat(100) dec3 := dec1.Mul(dec2) return dec3 } func TidbDecimalCase1() *types.MyDecimal { dec1 := new(types.MyDecimal) err := dec1.FromString(case1Bytes) if err != nil { log.Fatal(err) } dec2 := new(types.MyDecimal).FromInt(100) dec3 := new(types.MyDecimal) err = types.DecimalMul(dec1, dec2, dec3) if err != nil { log.Fatal(err) } return dec3 } func ShopspringDecimalCase2() decimal.Decimal { dec1, err := decimal.NewFromString(case2String) if err != nil { log.Fatal(err) } dec2 := decimal.NewFromFloat(0.1) for i := 0; i < 10; i++ { dec1 = dec1.Add(dec2) } return dec1 } func TidbDecimalCase2() *types.MyDecimal { dec1 := new(types.MyDecimal) dec1.FromString(case2Bytes) dec2 := new(types.MyDecimal) dec2.FromFloat64(0.1) for i := 0; i < 10; i++ { types.DecimalAdd(dec1, dec2, dec1) } return dec1 } // case1: 135.90*100 ==== func BenchmarkShopspringDecimalCase1(b *testing.B) { for i := 0; i < b.N; i++ { ShopspringDecimalCase1() } b.Log(ShopspringDecimalCase1()) // output: 13590 } func BenchmarkTidbDecimalCase1(b *testing.B) { for i := 0; i < b.N; i++ { TidbDecimalCase1() } b.Log(TidbDecimalCase1()) // output: 13590.00 } // case2: 0.1 add 10 times === func BenchmarkShopspringDecimalCase2(b *testing.B) { for i := 0; i < b.N; i++ { ShopspringDecimalCase2() } b.Log(ShopspringDecimalCase2()) // output: 1 } func BenchmarkTidbDecimalCase2(b *testing.B) { for i := 0; i < b.N; i++ { TidbDecimalCase2() } b.Log(TidbDecimalCase2()) // output: 1.0 } BenchmarkShopspringDecimalCase1-8 2000000 664 ns/op 340 B/op 10 allocs/op BenchmarkTidbDecimalCase1-8 20000000 99.2 ns/op 48 B/op 1 allocs/op BenchmarkShopspringDecimalCase2-8 300000 5210 ns/op 4294 B/op 111 allocs/op BenchmarkTidbDecimalCase2-8 3000000 517 ns/op 83 B/op 3 allocs/op 可见两种实现在上面两个case下表示准确, TiDB的decimal实现的性能高于shopspring的实现, 堆内存分配次数也更少. ##4. MyDecimal的已知问题 用了一段时间后, tidb.MyDecimal也有一些问题 原版除法有bug, 可以通过除数和被除数同时放大一定倍数临时修复, 更好的解决方法需要官方人员解决, 已提issue, 这个bug真是匪夷所思. https://github.com/pingcap/tidb/issues/4873, 2017.11.3官方修复decimal除法问题:https://github.com/pingcap/tidb/pull/4995/files. 原版乘法有小问题, 行为不一致, 原版的from1和to不能为同一个指针, 但 Add Sub Div却可以. 可以通过copy参数修复. 移位小坑, 右移属于扩大数值, 没有问题. 左移有问题, 注意1左移两位不会变成0.01, 所以shift不要传负数. round, 目前这个库的Round模式ModeHalfEven实际上是ModeHalfUp, 正常的四舍五入, 不是float的ModeHalfEven. 3.5=>4, 4.5=>5, 5.5=>6, 注意后期是否有变更.

2017/8/27
articleCard.readMore

DockerContainer下gdb无法正常工作的解决办法

昨天想在Mac上使用gdb调试一个Linux下编译的动态链接库, 以正常选项启动一个docker container, 运行gdb却发现如下错误提示. warning: Error disabling address space randomization: Operation not permitted Cannot create process: Operation not permitted During startup program exited with code 127. (gdb) 在google搜索结果里第6个才找到正确答案, https://www.google.com/search?safe=off&q=docker+gdb+warning%3A+Error+disabling+address+space+randomization%3A+Operation+not+permitted+Cannot+create+process%3A+Operation+not+permitted+During+startup+program+exited+with+code+127&oq=docker+gdb+warning%3A+Error+disabling+address+space+randomization%3A+Operation+not+permitted+Cannot+create+process%3A+Operation+not+permitted+During+startup+program+exited+with+code+127, 原来是docker run中的一个不太常用的选项, docker run –privileged, 加上即可. 于是找官方文档查看此选项的解释, 了解到: 默认docker是以受限模式下运行container, 如不能在container中运行再运行一个docker, 不能访问宿主机上的真实设备, /dev/, gdb无法访问真实的内存设备. Runtime privilege and Linux capabilities >--cap-add: Add Linux capabilities >--cap-drop: Drop Linux capabilities >--privileged=false: Give extended privileges to this container >--device=[]: Allows you to run devices inside the container without the --privileged flag. > >By default, Docker containers are “unprivileged” and cannot, for example, run a Docker daemon inside a Docker container. This is because by default a container is not allowed to access any devices, but a “privileged” container is given access to all devices (see the documentation on cgroups devices). >When the operator executes docker run --privileged, Docker will enable access to all devices on the host as well as set some configuration in AppArmor or SELinux to allow the container nearly all the same access to the host as processes running outside containers on the host. Additional information about running with --privileged is available on the Docker Blog. >If you want to limit access to a specific device or devices you can use the --device flag. It allows you to specify one or more devices that will be accessible within the container. > $ docker run –device=/dev/snd:/dev/snd …

2017/8/20
articleCard.readMore

Go sync.Pool Slice Benchmark

纠结于[]struct还是[]*struct 直接make([]struct,0) 后append 还是 用sync.Pool make([]struct,100) 写段代码跑个分, 结论是 []*struct的要比[]struct多n次取指针的内存分配, 所有更慢, 如果不用修改结构体元素内的值, 没有必要用指针切片 append[]*struct要比[]struct慢 sync.Pool效果明显 benchmark结果 BenchmarkStructSliceWithoutPool-8 200000 5458 ns/op 16320 B/op 8 allocs/op BenchmarkStructPointerSliceWithoutPool-8 200000 6045 ns/op 8504 B/op 109 allocs/op BenchmarkStructSliceWithPool-8 1000000 1287 ns/op 32 B/op 1 allocs/op BenchmarkStructPointerSliceWithPool-8 300000 4910 ns/op 6498 B/op 102 allocs/op benchmark代码 package main import ( "sync" "testing" ) var structSlicePool = sync.Pool{ New: func() interface{} { return make([]Basic, 100) }, } var structPointerSlicePool = sync.Pool{ New: func() interface{} { return make([]*Basic, 100) }, } type Basic struct { Id, N1, N2, N3, N4, N5 int Name string } func BenchmarkStructSliceWithoutPool(b *testing.B) { for i := 0; i < b.N; i++ { var list []Basic for j := 0; j < 101; j++ { var data = Basic{Id: j, Name: "Name"} list = append(list, data) } } } func BenchmarkStructPointerSliceWithoutPool(b *testing.B) { for i := 0; i < b.N; i++ { var list []*Basic for j := 0; j < 101; j++ { var data = Basic{Id: j, Name: "Name"} list = append(list, &data) } } } func BenchmarkStructSliceWithPool(b *testing.B) { for i := 0; i < b.N; i++ { list := structSlicePool.Get().([]Basic) initLen := len(list) for j := 0; j < 101; j++ { var data = Basic{Id: j, Name: "Name"} if j < initLen { list[j] = data } else { list = append(list, data) } } structSlicePool.Put(list) } } func BenchmarkStructPointerSliceWithPool(b *testing.B) { for i := 0; i < b.N; i++ { list := structPointerSlicePool.Get().([]*Basic) initLen := len(list) for j := 0; j < 101; j++ { var data = Basic{Id: j, Name: "Name"} if j < initLen { list[j] = &data } else { list = append(list, &data) } } structPointerSlicePool.Put(list) } }

2017/7/2
articleCard.readMore

Go最佳实践

来自NSQ nsq的官方文档的Dsign中提到一个PPThttps://speakerdeck.com/snakes/nsq-nyc-golang-meetup, 里面有这样一段话 总结一下. don’t be afraid of sync package sync包里有 sync.Mutex(互斥锁,一读一写) sync.RWMutex(读写锁,可以多读一写) sync.Pool(对象池, 合理利用可以减少内存分配, 降低GC压力, 稍后写一篇博客说说) sync.Once(并发控制. 适用于开几个goroutines去执行一个只执行一次的任务, 比如单例模式) sync.Cond(并发控制, cond.Wait()阻塞至其他goroutie运行到cond.Signal()) sync.WaitGroup(并发控制. 通常用法 wg.Add增加任务数量 goroutie完成任务后执行wg.Done,任务数量减1 wg.Wait等待wg任务数量为0) goroutines are cheap not free 这句话在其他地方也看过, go func()简单好用, 创建开销也很小, 但也是有开销的. 很多情况下开固定数量worker, 用channel传递数据, 效果会更好. go-apns2中的example是个非常好的例子.https://github.com/sideshow/apns2/blob/master/_example/channel/main.go 注意一个问题, go里面一个goroutine panic了, 会导致进程退出, 所以go func()时第一行带上 go func(){ defer func(){ if err:=recover(); err!=nil{ } }() }() 是安全的做法, worker channel法时类似 package main import ( "fmt" "log" "time" ) func main() { ch := make(chan int, 10) for i := 0; i < 2; i++ { go worker(ch, i) } for i := 0; i < 3; i++ { ch <- i ch <- -1 } time.Sleep(time.Second * 5) } func worker(ch <-chan int, goId int) { log.Printf("worker%d running", goId) for data := range ch { func() { defer func() { if err := recover(); err != nil { log.Printf("worker%d recover error:%s", goId, err) } }() log.Printf("worker%d received data:%d", goId, data) if data == -1 { panic(fmt.Errorf("worker%d panic", goId)) } }() } } fasthttp之所以快, 其中一个原因就是net/http是来一个连接就创建一个goroutie, 而fasthttp用了池复用了goroutines. watch your allocations (string() is costly, re-user buffers) go里面 []byte和string互转是会发生复制的, 开销明显, 如果代码里频繁互转, 考虑使用bytes.buffer 和 sync.Pool use anonymous structs for arbitrary JSON 在写http api时, parse body这种事情, 如果只是纯粹取body里的json数据, 没必要单独定义结构体, 在函数里定义一个匿名结构体就好. var s struct { A int} no built-in per-request HTTP timeouts 这是说要注意默认的httpClient没有超时 synchronizing goroutine exit is hard - log each cleanup step in long-running goroutines 同步化的goroutine一不小心就没有退出, 如果你写一个长期运行的服务, 用logger记录每一个goroutine的清理退出, 防止goroutine泄露 select skips nil channels select语句是会跳过nil的channels的. 因为在Go里往已经close掉的channel里发送数据是会panic的, 可以利用select语句. 附: channel操作导致panic的情况有: 关闭一个nil的channel, 关闭一个已经关闭的channel( j,ok:= <- ch, ok为false时代表ch已经关闭了), 往一个已经关闭的channel里发送数据(从已经关闭的channel里读数据是OK的, 如果这个channel是带缓冲的, 那么可以读到所有数据) 来自GO箴言 Python有import this的zen of Python, 想不到Go也有箴言 https://speakerdeck.com/ajstarks/go-proverbs 在go里, goroutines之间通信不要用共享内存的方式实现, 应该用channel来实现 并发不是并行 channel是编排, mutexs是串行 interface定义越多的方法, 抽象程度越低. Go提倡用接口组合的方式实现更大的接口 零值, 猜测这里说的是struct{}吧, struct{}是一个不占内存的空结构体, 在用map实现set, channel发送无额外意义的signal时能降低内存分配 提倡gofmt 一点点复制比一点点依赖好. 官方包里有时能见到一些复制的代码, 这是为了不互相依赖 syscall每个平台实现不一样, 要加build tags cgo每个平台的lib不一样, 要加build tags Cgo不是go unsafe包不提供保障 简洁胜过高效 error是值 可以用值的方式去处理错误: 传递, 比较 不用仅检查错误, 要优雅地处理 多花精力设计架构, 模块命名, 写详细的文档 写良好的文档给用户 对于普通错误, 应该用多值返回错误, 而不是手动panic 实践 写可重复使用的函数, 接收接口类型, 返回具体类型 写可扩展便于二次包装的函数, 接收接口类型, 返回接口类型. 如标准库的database/sql包 传值还是传指针 指针仅用于要修改值的场景和反射, 其他场景尽可能地用值传递, 能让变量尽可能分配在栈上, 减少GC压力, 提高性能. 还能减少nil 参考 https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/ Golang Github https://github.com/golang/go/wiki/CodeReviewComments

2017/6/24
articleCard.readMore

GO Logger 日志实践

分析一下用过的打印日志的log包 Go标准库自带log, 这个log的func比较少, 没有区分level, 但足够简单, 有prefix功能, 可以设置flag来控制时间格式, caller的文件名和行数, 其它的标准包如 net/http database/sql 等也用了此包. 对自带的log进行包装, 加入level, 颜色. 如ngaut/log, 这个log star数并不多, 还是从最近很火的一个项目pingcap/tidb里看到的, 有点小清新的感觉, 但这个log可能只是为tidb使用的, 缺少自带log的一些方法, 导致没法使用在一些可以定制logger的第三方库如gorm中. 于是我fork了一下https://github.com/hanjm/log, 增加了一些方法, 以便可以给gorm用. 完全自己实现的log, 结构化输出, 通常是key=value或json, 有名的有logrus, zap等. 第一次看到logrus感觉美极了, 于是大量使用, 直到在关注tidb的时候收到带日志的issues邮件, 里面的日志带了caller, 感觉很有用, 于是去搜logrus的issue看有没有这个功能, 搜到了一个issuehttps://github.com/sirupsen/logrus/issues/63, 讨论了三年这个功能还没加上, 只好放弃美丽的logrus, 找到了替代品zap, zap的设计非常好, 定制性强. log是经常调用的代码, 每次调用不可避免地要进行内存分配, 分配次数和每次分配的内存大小将影响性能. 对log内容的处理也是一个涉及到性能的点, 像log.Printf参数是interface{}, logrus的field是map[stirng]interface{}, 打印interface{}只能靠reflect, Go是静态强类型语言, 用反射的开销相对比较大, 所以zap使用了手动指定类型的方式, 从zap提供的benchmark上开看, 性能提升还是蛮大的, 虽然相比logrus使用起来更麻烦, 但为了性能, 还是值得的. 总结 为了方便进行日志分析, 统一用json行日志, 这样用elk时可以免去定制正则表达式存储到elasticSearch的field中. net/http database/sql 及一些第三方包可能直接使用了标注库的log, 有个trick可以改变所有使用标准包log的行为, 通过log.SetOutput(w io.Writer)来改变位置, w是一个实现了Write(p []byte) (n int, err error)方法的io.Writer即可. runtime.Caller可以得到调用者的pc, 文件名, 文件行数, runtime.FuncForPC(pc).Name()可以得到pc所在的函数名, 对于debug非常有帮助. 但有一定性能开销, 所以方案是: 对于http server的access log, 没有必要使用带caller的日志, 而对于http api具体实现的函数内的log, 有必要记录caller, 而且光有文件名和行数还不够, 毕竟改了代码行数就变了, 而函数名一般不会变, 带上函数名会更直观. GitHub搜了一圈, 好多公司都会定制自己的log, 如tidb的ngaut/log, 七牛的qiniu/log, 饿了么的eleme/log, mailgun的mailgun/log, 是的, 我也造一个小轮子zaplog.zaplog是包装了zap, 带caller func name, 兼容logrus stdlog 的日志输出工具. package zaplog import ( "bytes" "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" "log" "runtime" "strings" ) // CallerEncoder will add caller to log. format is "filename:lineNum:funcName", e.g:"zaplog/zaplog_test.go:15:zaplog.TestNewLogger" func CallerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(strings.Join([]string{caller.TrimmedPath(), runtime.FuncForPC(caller.PC).Name()}, ":")) } func newLoggerConfig(debugLevel bool) (loggerConfig zap.Config) { loggerConfig = zap.NewProductionConfig() loggerConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder loggerConfig.EncoderConfig.EncodeCaller = CallerEncoder if debugLevel { loggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) } return } // NewCustomLoggers is a shortcut to get normal logger, noCallerLogger. func NewCustomLoggers(debugLevel bool) (logger, noCallerLogger *zap.Logger) { loggerConfig := newLoggerConfig(debugLevel) logger, err := loggerConfig.Build() if err != nil { panic(err) } loggerConfig.DisableCaller = true noCallerLogger, err = loggerConfig.Build() if err != nil { panic(err) } return } // NewLogger return a normal logger func NewLogger(debugLevel bool) (logger *zap.Logger) { loggerConfig := newLoggerConfig(debugLevel) logger, err := loggerConfig.Build() if err != nil { panic(err) } return } // NewNoCallerLogger return a no caller key value, will be faster func NewNoCallerLogger(debugLevel bool) (noCallerLogger *zap.Logger) { loggerConfig := newLoggerConfig(debugLevel) loggerConfig.DisableCaller = true noCallerLogger, err := loggerConfig.Build() if err != nil { panic(err) } return } // CompatibleLogger is a logger which compatible to logrus/std log/prometheus. // it implements Print() Println() Printf() Dbug() Debugln() Debugf() Info() Infoln() Infof() Warn() Warnln() Warnf() // Error() Errorln() Errorf() Fatal() Fataln() Fatalf() Panic() Panicln() Panicf() With() WithField() WithFields() type CompatibleLogger struct { _log *zap.Logger } // NewCompatibleLogger return CompatibleLogger with caller field func NewCompatibleLogger(debugLevel bool) *CompatibleLogger { return &CompatibleLogger{NewLogger(debugLevel).WithOptions(zap.AddCallerSkip(1))} } // Print logs a message at level Info on the compatibleLogger. func (l CompatibleLogger) Print(args ...interface{}) { l._log.Info(fmt.Sprint(args...)) } // Println logs a message at level Info on the compatibleLogger. func (l CompatibleLogger) Println(args ...interface{}) { l._log.Info(fmt.Sprint(args...)) } // Printf logs a message at level Info on the compatibleLogger. func (l CompatibleLogger) Printf(format string, args ...interface{}) { l._log.Info(fmt.Sprintf(format, args...)) } // Debug logs a message at level Debug on the compatibleLogger. func (l CompatibleLogger) Debug(args ...interface{}) { l._log.Debug(fmt.Sprint(args...)) } // Debugln logs a message at level Debug on the compatibleLogger. func (l CompatibleLogger) Debugln(args ...interface{}) { l._log.Debug(fmt.Sprint(args...)) } // Debugf logs a message at level Debug on the compatibleLogger. func (l CompatibleLogger) Debugf(format string, args ...interface{}) { l._log.Debug(fmt.Sprintf(format, args...)) } // Info logs a message at level Info on the compatibleLogger. func (l CompatibleLogger) Info(args ...interface{}) { l._log.Info(fmt.Sprint(args...)) } // Infoln logs a message at level Info on the compatibleLogger. func (l CompatibleLogger) Infoln(args ...interface{}) { l._log.Info(fmt.Sprint(args...)) } // Infof logs a message at level Info on the compatibleLogger. func (l CompatibleLogger) Infof(format string, args ...interface{}) { l._log.Info(fmt.Sprintf(format, args...)) } // Warn logs a message at level Warn on the compatibleLogger. func (l CompatibleLogger) Warn(args ...interface{}) { l._log.Warn(fmt.Sprint(args...)) } // Warnln logs a message at level Warn on the compatibleLogger. func (l CompatibleLogger) Warnln(args ...interface{}) { l._log.Warn(fmt.Sprint(args...)) } // Warnf logs a message at level Warn on the compatibleLogger. func (l CompatibleLogger) Warnf(format string, args ...interface{}) { l._log.Warn(fmt.Sprintf(format, args...)) } // Error logs a message at level Error on the compatibleLogger. func (l CompatibleLogger) Error(args ...interface{}) { l._log.Error(fmt.Sprint(args...)) } // Errorln logs a message at level Error on the compatibleLogger. func (l CompatibleLogger) Errorln(args ...interface{}) { l._log.Error(fmt.Sprint(args...)) } // Errorf logs a message at level Error on the compatibleLogger. func (l CompatibleLogger) Errorf(format string, args ...interface{}) { l._log.Error(fmt.Sprintf(format, args...)) } // Fatal logs a message at level Fatal on the compatibleLogger. func (l CompatibleLogger) Fatal(args ...interface{}) { l._log.Fatal(fmt.Sprint(args...)) } // Fatalln logs a message at level Fatal on the compatibleLogger. func (l CompatibleLogger) Fatalln(args ...interface{}) { l._log.Fatal(fmt.Sprint(args...)) } // Fatalf logs a message at level Fatal on the compatibleLogger. func (l CompatibleLogger) Fatalf(format string, args ...interface{}) { l._log.Fatal(fmt.Sprintf(format, args...)) } // Panic logs a message at level Painc on the compatibleLogger. func (l CompatibleLogger) Panic(args ...interface{}) { l._log.Panic(fmt.Sprint(args...)) } // Panicln logs a message at level Painc on the compatibleLogger. func (l CompatibleLogger) Panicln(args ...interface{}) { l._log.Panic(fmt.Sprint(args...)) } // Panicf logs a message at level Painc on the compatibleLogger. func (l CompatibleLogger) Panicf(format string, args ...interface{}) { l._log.Panic(fmt.Sprintf(format, args...)) } // With return a logger with an extra field. func (l *CompatibleLogger) With(key string, value interface{}) *CompatibleLogger { return &CompatibleLogger{l._log.With(zap.Any(key, value))} } // WithField return a logger with an extra field. func (l *CompatibleLogger) WithField(key string, value interface{}) *CompatibleLogger { return &CompatibleLogger{l._log.With(zap.Any(key, value))} } // WithFields return a logger with extra fields. func (l *CompatibleLogger) WithFields(fields map[string]interface{}) *CompatibleLogger { i := 0 var clog *CompatibleLogger for k, v := range fields { if i == 0 { clog = l.WithField(k, v) } else { clog = clog.WithField(k, v) } i++ } return clog } // FormatStdLog set the output of stand package log to zaplog func FormatStdLog() { log.SetFlags(log.Llongfile) log.SetOutput(&logWriter{NewNoCallerLogger(false)}) } type logWriter struct { logger *zap.Logger } // Write implement io.Writer, as std log's output func (w logWriter) Write(p []byte) (n int, err error) { i := bytes.Index(p, []byte(":")) + 1 j := bytes.Index(p[i:], []byte(":")) + 1 + i caller := bytes.TrimRight(p[:j], ":") // find last index of / i = bytes.LastIndex(caller, []byte("/")) // find penultimate index of / i = bytes.LastIndex(caller[:i], []byte("/")) w.logger.Info("stdLog", zap.ByteString("caller", caller[i+1:]), zap.ByteString("log", bytes.TrimSpace(p[j:]))) return len(p), nil }

2017/5/19
articleCard.readMore

Linux Cli下酷工具收集(持续)

mycli. ipython之于Python, mycli之于mysql. 当然还有pgcli. Python写的工具. Github地址:https://github.com/dbcli/mycli youtube-dl. youtube下载器, 能直接下载youtube视频列表. Python写的工具. oh-my-zsh的z命令. 手动输入一个很长的路径名不停地tab很麻烦, 配置了oh-my-zsh的话可以启用z命令(edit: ~/.zshrc line: plugins=(git python z tmux)), z 文件夹名 就可以跳转到常用目录中最符合输入文件夹名的文件夹中, 非常方便, GitHub地址https://github.com/robbyrussell/oh-my-zsh privoxy. brew pip npm install、docker pull总是慢如蜗牛? privoxy能将shadowsocks的socks代理(127.0.0.1:1080)转换为http/https代理, 有个奇特的地方是把它把文档写在配置文件的注释里, config文件有2271行, 初让人以为配置起来会巨复杂, 实际上基本的功能两行配置即可. listen-address配置为0.0.0.0:8118, 局域网内其他设备也可以走此代理:8118. 官网https://www.privoxy.org/ listen-address 0.0.0.0:8118 forward-socks5 / localhost:1080 . 然后在.zshrc或.bashrc中加入一下命令就可以通过proxy dproxy来切换是否在本终端下使用代理了. function proxy(){ export http_proxy=http://127.0.0.1:8118 export https_proxy=https://127.0.0.1:8118 } function dproxy(){ unset http_proxy unset https_proxy } kcptun. bandwagon上的shadowsocks越来越慢, 不用kcptun加速没法正常使用, 只能不太厚道地超量发包了. GitHub地址https://github.com/xtaci/kcptun

2017/5/6
articleCard.readMore

MacOS下酷工具收集(持续)

LaunchRocket. 系统偏好设置中的用户与群组可以添加登录项脚本, 但对于需要root权限启动的应用无解, 脚本会卡在输入密码. LaunchRocket能优雅解决.(当然, 还有一种魔法方法: 用docker加--restart always的container或docker swarm service 跑, docker启动时会自动启动这些container/service) GitHub地址https://github.com/jimbojsb/launchrocket Snip. Windows下有Winsnap、FastStone、Snipaste等此等优秀的截图软件, 相比之下, Mac上的截图软件要逊色不少. Winsnap有个很方便的特性是截图可以默认复制到剪切板, 很容易粘贴到其他软件去, Mac上snip也有这个特性, snip是一款腾讯开发的截图软件, 官网http://snip.qq.com/ jietu. 还是腾讯开发的截图软件, 比snip更好用, 也是默认复制到剪切板, 而且提供编辑功能. 官网http://jietu.qq.com Macdown. 好用优雅免费的Markdown书写工具. GitHub地址https://github.com/MacDownApp/macdown 2018.12.15发现了更棒的Markdown软件Typora, 胜在颜值和文件夹模式. Homebrew. yum/dnf之于centOS, apt之于Ubuntu, pacman之于ArchLinux, brew之于macOS, brew install简直不能太爽. 官网https://brew.sh/ Homebrew-Cask. brew install不能安装chrome Macdown这样的GUI app, Homebrew-Cask扩展了brew, GitHub地址https://github.com/caskroom/homebrew-cask privoxy. brew pip npm install、docker pull总是慢如蜗牛? privoxy能将shadowsocks的socks代理(127.0.0.1:1080)转换为http/https代理, 有个奇特的地方是把它把文档写在配置文件的注释里, config文件有2271行, 初让人以为配置起来会巨复杂, 实际上基本的功能两行配置即可. listen-address配置为0.0.0.0:8118, 局域网内其他设备也可以走此代理:8118. 官网https://www.privoxy.org/ listen-address 0.0.0.0:8118 forward-socks5 / localhost:1080 . 然后在.zshrc或.bashrc中加入一下命令就可以通过proxy dproxy来切换是否在本终端下使用代理了. function proxy(){ export http_proxy=http://127.0.0.1:8118 export https_proxy=https://127.0.0.1:8118 export HTTP_PROXY=http://127.0.0.1:8118 export HTTPS_PROXY=https://127.0.0.1:8118 } function dproxy(){ unset http_proxy unset https_proxy unset HTTP_PROXY unset HTTPS_PROXY } proxier 可以按进程名指定哪些进程走ss代理, 比如ssh kcptun. bandwagon上的shadowsocks越来越慢, 不用kcptun加速没法正常使用, 只能不太厚道地超量发包了. GitHub地址https://github.com/xtaci/kcptun oh-my-zsh的z命令. 手动输入一个很长的路径名不停地tab很麻烦, 配置了oh-my-zsh的话可以启用z命令(edit: ~/.zshrc line: plugins=(git python z tmux)), z 文件夹名 就可以跳转到常用目录中最符合输入文件夹名的文件夹中, 非常方便, GitHub地址https://github.com/robbyrussell/oh-my-zsh aria2. 非常厉害的下载器, brew install aria2即可. GitHub地址:https://github.com/aria2/aria2 GUI Client:https://github.com/yangshun1029/aria2gui BaiduExporter. 百度网盘文件导出到aria2下载, GitHub地址https://github.com/acgotaku/BaiduExporter Chrome插件加白描述文件. 有时需要安装一些在商店下架了的扩展, 比如BaiduExporter, 在扩展页面安装后重启chrome会提示此扩展程序并非来自chrome商店,启用开关灰色无法启用, 可以下载此描述文件将特定扩展id加入白名单. https://hencolle.com/2016/10/16/baidu_exporter/ 坚果云.同步配置文件,pdf书非常方便. Monaco字体. 非常舒服的等宽代码字体 https://github.com/hanjm/codeFont Scroll Reverser. 外接鼠标神器, 让触摸板是自然方向, 鼠标是习惯的Windows滚动方向. Karabiner-Elements. 外接键盘神器, 我就想禁用capsLock键, 让右边的option键变成control键, 能把外接键盘的Windows键映射为command键. ssh tunnel. ssh隧道管理器, 本地连接远程服务器的kibana nsqAdmin mysql简直不要太爽

2017/5/6
articleCard.readMore

知名公司架构资料整理(持续)

1.知乎 主要语言栈: Python Java 演讲: http://www.infoq.com/cn/news/2014/12/zhihu-architecture-evolution 2.饿了么 主要语言栈: Python Java Go 演讲: 2017 gopher上海meetup 开源项目: Python: thrift-py RPC轮子 3.Bilibili 开源项目: goim 4. Teambition 主要语言栈: node.js Go 演讲: 2017 gopher上海meetup 开源项目: Go: Gear HTTP框架 5. Klook 主要语言栈: Go 6. PingCAP 主要语言栈: GO 开源项目: Go: TiDB 数据库 Go: log 日志工具 https://github.com/davideuler/architecture.of.internet-product

2017/5/4
articleCard.readMore

Mysql 连接池问题

最近应用日志里发现了mysql偶尔会出现问题 [mysql] 2017/04/26 10:01:05 packets.go:130: write tcp 127.0.0.1:56346->127.0.0.1:3306: write: broken pipe [mysql] 2017/04/26 10:01:05 packets.go:130: write tcp 127.0.0.1:56346->127.0.0.1:3306: write: broken pipe [mysql] 2017/04/26 10:01:05 packets.go:130: write tcp 127.0.0.1:56350->127.0.0.1:3306: write: broken pipe [mysql] 2017/04/26 10:01:05 packets.go:130: write tcp 127.0.0.1:56350->127.0.0.1:3306: write: broken pipe 找GitHub issues, 提到了和mysql的wait_timeout变量有关系, https://github.com/go-sql-driver/mysql/issues/446, 于是找MySQL文档https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#idm140549060476496. 相关说明如下: The number of seconds the server waits for activity on a noninteractive connection before closing it. On thread startup, the session wait_timeout value is initialized from the global wait_timeout value or from the global interactive_timeout value, depending on the type of client (as defined by the CLIENT_INTERACTIVE connect option to mysql_real_connect()). See also interactive_timeout. 默认是28800s, 8小时. mysql> show variables like '%wait_timeout%'; +--------------------------+----------+ | Variable_name | Value | +--------------------------+----------+ | innodb_lock_wait_timeout | 50 | | lock_wait_timeout | 31536000 | | wait_timeout | 28800 | +--------------------------+----------+ 3 rows in set (0.00 sec) 解决办法: db.SetConnMaxLifetime(time.Hour*7)

2017/4/26
articleCard.readMore

Go strings.TrimLeft() strings.TrimPrefix().md

今天在调试时, 有个函数的返回的结果很奇怪, 和预期的输入差了一个字符, 而review代码时却没发现什么问题, 后面各种加logger.Debugf()才发现是strings.TrimLeft()这个函数表现得和自己的预期不一致, 从函数名上看这个是删除字符串左边的字符串, 但是传入一个带:的字符串去调用,发现:后面的字符也被Trim了, 于是去Github issues上搜了下这个问题https://github.com/golang/go/issues/19371, 有人也感觉奇怪也反馈过, 解释是 The second argument to Trim is a set of code points, not a prefix/suffix. , 于是去翻了下文档, 确实是这样的. TrimLeft returns a slice of the string s with all leading Unicode code points contained in cutset removed. 问题复现代码(go 1.8) https://play.golang.org/p/YtmVQIf2_i: package main import ( "fmt" "strings" ) func main() { str := "friends:d15fc7bb-1e67-11e7-b8a5-00163e008796" prefix1 := "friends:" prefix2 := "friends" fmt.Printf("%v\n", strings.TrimLeft(str, prefix1)) fmt.Printf("%v\n", strings.TrimPrefix(str, prefix1)) fmt.Printf("%v\n", strings.TrimLeft(str, prefix2)) fmt.Printf("%v\n", strings.TrimPrefix(str, prefix2)) } output: 15fc7bb-1e67-11e7-b8a5-00163e008796 d15fc7bb-1e67-11e7-b8a5-00163e008796 :d15fc7bb-1e67-11e7-b8a5-00163e008796 :d15fc7bb-1e67-11e7-b8a5-00163e008796

2017/4/24
articleCard.readMore