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: |
前面说到, 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是计数器, 单调递增的, 只有服务重启时才会清零, 比如http请求数, errorLevel的log数. 值得一提的是, prometheus的内置函数求值时会自动处理重启清零的情况.
counter的value是float64, 怎么无锁地操作float64呢? 答案是用math包将其视作uint64来操作.
func (v *value) Add(val float64) { |
Guage是一个可增可减的数值指标, 比如CPU使用率, 内存使用率, 协程数.
Historygram是直方图, 适合需要知道数值分布范围的场景, 比如http请求的响应时长, http请求的响应包体大小等.
直方图的组距不一定是固定的, 可以自己定义适合, 这里称其为bucket, 每一个metric value根据其数值大小落在对应的bucket.
Historygram实际上包含多个时序数据.
<basename>_bucket{le="<upper inclusive bound>"}
小于等于指定数值的计数.<basename>_sum
总和<basename>_count
总计数, 其值当然也等于<basename>_bucket{le="+Inf"}
Summary相比Historygram是按百分位聚合好的直方图, 适合需要知道百分比分布范围的场景, 比如对于 http请求的响应时长, Historygram是侧重在于统计小于1ms的请求有多少个, 1ms~10ms的请求有多少个, 10ms以上的请求有多少个, 而Summary在于统计20%的请求的响应时间是多少, 50%的请求的响应时间是多少, 99%的请求的响应时间是多少. Historygram是计数原始数据, 开销小, 执行查询时有对应的函数计算得到p50, p99, 而Summary是在客户端SDK测做了聚合计算得到指定的百分位, 开销更大一些.
prometheus的Golang SDK设计得很地道, 充分利用了GO语言的特性.
在SDK中所有的指标类型都实现了prometheus.Collector
接口.
// Collector is the interface implemented by anything that can be used by |
prometheus.Collector
接口中的方法传参都是只写的chan
, 使得实现接口的代码无论是同步还是并行都可以. Describe(chan<- *Desc)
方法是在将Collector注册或注销时调用的, Collect(chan<- Metric)
方法是在被抓取收集指标时调用的.
不带label的指标类型使用prometheus.NewCounter
prometheus.NewGauge
prometheus.NewHistogram
prometheus.NewSummary
去创建并使用prometheus.MustRegister
注册, 一般是初始化好作为一个包内全局变量, 在init函数中注册.
var ( |
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 { |
在编辑图表写查询语句时,不会显示指标类型, 所以最好看到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 |
对于时长time.Duration数据类型的指标值收集, time.Since是优化过的, 直接走runtimeNano, 无需走系统调用取当前时间, 性能优于time.Now后相减, 另外, 频繁调用time.Now在性能要求高的程序中也会变成不小的开销.
Prometheus查询语句(PromQL)是一个相比SQL更简单也很有表达力的专用查询语言, 通过文档及例子学习.
Prometheus自带的Graph面板比较简陋, 一般情况下直接用强大的Grafana就行了, 制作图表dashboard时, 直接输入PromQL即可展示时序图表.
http_requests_total{job="prometheus",group="canary"} |
查询条件中,除了=和!=外, =~表示正则匹配, !~表示正则不匹配.
查询条件也可以作用在metric name上, 语法有点像Python的__前缀的魔法, 如用 {__name__=~"job:.*"}
表示选择名字符合job:.*
这样的正则的metric.
http_requests_total{job="prometheus"}[5m] |
范围条件中, 时长字符串语法和GO一样, s代表秒, m代表分, h代表小时, d代表天, w代表星期, y代表年.
changes()
变化次数delta(v range-vector)
平均变化量, 只适用于guageidelta(v range-vector)
即时变化量, 只适用于guagehistogram_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)
增量, 只适用于counterrate
- 平均QPSirate
- 即时QPS, 如果原始数据变化快, 可以使用更敏感的irate这里列举一些我通过搜索及自行摸索出来的对于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处可视化界面设置会比较简单, 可设置连续多少次指定的promQL查出的值不在指定的范围即触发报警, 告警通知的最佳搭配当然是slack channel.
]]>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 |
ActiveMQ可以将本身的一些事件投递到系统的消息队列, 如 queue/topic的创建, 没有消费者的queue/topic等. http://activemq.apache.org/advisory-message.html
这个特性对于监控MQ非常有用, 默认配置时关闭的, 需要在配置文件activemq.xml中打开.
通配符
. 用于分割名字中的多个单词 |
通配符可以用在配置文件中表名作用范围, 也可以用于订阅时的destination名字, 这个功能很不错.
所谓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> |
ActiveMQ支持延时消息及定时消息, 在message header中带上如下字段即可, 其中AMQ_SCHEDULED_PERIOD的最大值是long的最大值, 所以可以设置延时很长时间.
Property name | type | description |
---|---|---|
AMQ_SCHEDULED_DELAY | long | The time in milliseconds that a message will wait before being scheduled to be delivered by the broker |
AMQ_SCHEDULED_PERIOD | long | The time in milliseconds to wait after the start time to wait before scheduling the message again |
AMQ_SCHEDULED_REPEAT | int | The number of times to repeat scheduling a message for delivery |
AMQ_SCHEDULED_CRON | String | Use a Cron entry to set the schedule |
如果broker投递给消费者消息, 没有ACK或NACK, 则会触发重新投递, 投递超过一定次数则会进入死信队列, 默认只有一个公共的死信队列ActiveMQ.DLQ, 如果需要给topic分别设置死信队列, 则要在修改activemq.xml.
<broker> |
默认非持久化的topic不会进入到死信队列中, 如果需要, 则修改activemq.xml, 加入
<!-- |
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>, 非常得简洁, 一页就讲完了.
支持 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
https://github.com/go-stomp/stomp 是目前star数最高的
package main |
协议文档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
/
+
#
分别对应 ActiveMQ的.
*
>
.协议文档: http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-overview-v1.0-os.html
AMQP相比 stomp mqtt 就复杂得多, 毕竟名字就是高级消息队列(Advanced Message Queuing Protocol ).
maxDataLength
为 104857600
(100MB), maxFrameSize
为 MAX_LONG
, consumer持有的未确认最大消息数量prefetch
为1000, producerCredit
为10000. 可通过连接的URI设定.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 |
官方的FAQ里面写了一些实现的细节
如果使用了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
今天在Mac上的 dockercontainer里面启动一个服务, 这个服务需要连我主机上的MySQL, 用 172.17.0.1 是访问不了的, Connection refused.
root@d99939cc53fc:/tmp# curl 172.17.0.1:3306 |
但是看网络结构, 和Linux的一样, 也是在172.17段下的.
root@d99939cc53fc:/tmp# ip addr |
不得其解, 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 ofdocker.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 |
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用户 |
cd /usr/local |
2.[官网下载OpenSSL 1.0.2以上版本].https://github.com/openssl/openssl/releases
cd nginx-1.14.2 |
2.官网下载pcre
注意Nginx不支持pcre2,下载pcre最新版即可. 解压到Nginx解压的目录
cd nginx-1.14.2 |
4.官网下载zlib(version 1.1.3 — 1.2.11)
cd nginx-1.14.2 |
5.编译并安装
./configure \ |
6.为了方便操作,软链/usr/local/nginx/sbin/nginx到/usr/local/bin
ln -sf /usr/local/nginx/sbin/nginx /usr/local/bin |
github.com/go-sql-driver/mysql/connection.go:370 |
后续再补篇文章详细写写sql.Driver
]]>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头部的开销, 性能更好
库
创建数据库 |
表
显示该数据库中的表 |
查看数据保留策略 retention polices
SHOW RETENTION POLICIES ON "testDB" |
创建新的Retention Policies并设置为默认值
DURATION 保留多少天 |
创建一个连续查询, 每10秒计算一个接口响应耗时平均值到新表 |
grafana 是 influxdb 的良好伴侣, 写个query语句就能得到很炫酷的图形. 比如接口请求量图表按handler_name, response_code, time(10s) group by就得到了.
]]>在代码中import "google.golang.org/grpc/reflection"
包, 然后加一行代码reflection.Register(s)
, 就可以启用 server reflection. 就可以用grpc_cli去进行获得服务列表, 方法列表, message结构体定义了. reflection.Register(s)实际上是注册了一个特殊的service, 它能列出server中已注册的服务和方法等信息.
用 encoding.RegisterCompressor
方法取注册一个压缩器, 启用了压缩的话, 服务端和客户端双方都要进行同样的处理, 服务端在newServer时要带上compressor的serverOption, 客户端在dail的时候要带上WithDefaultCallOptions的DialOption, DialOption加上压缩解压的处理, 不然会得到一个 Internal error
, 和HTTP方式一样, 压缩类型体现在content-type的header上.
Dial得到的ClientConn是并发安全.
stream的读写不是并发安全的, sendMsg或RecvMsg不能在多个goroutine中并发地调用,但可以分别在两个goroutine中处理send和Recv.
自定义消息编码解码, 注册一个实现 Codec
接口的对象即可, 然后在Dial或Call时带上grpc.CallContentSubtype
这个CallOption, 这样就可以自动处理这个带这个content-type的请求. 默认为 proto
自定义压缩解压缩, 注册一个实现 Compressor
接口的对象即可, 然后在Dial或Call时带上grpc.UseCompressor
这个CallOption.
](https://github.com/grpc/grpc-go/blob/master/Documentation/gomock-example.md)
主要讲如何在单元测试中mock, 用gomock命令行生成实现xx接口的代码, 没什么特别的
](https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-auth-support.md)
主要讲如何进行身份验证, 没什么特别的
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
gRPC会定时发http2 ping帧来判断连接是否挂掉, 如果ping没有在一定时期内ack, 那么连接会被close.
grpc-go包默认用gpclog包打日志, grpclog包默认是用多个*log.Logger来实现日志级别, 默认输出到stderr, 对于生产环境, 肯定要集成到自己的日志流里去, 接口是个好东西, grpclog包允许setLog, 实现grpclog.LoggerV2接口即可.
grpclog里的info是为了debug
之前一直有个误区, 多个连接比单个连接要快, 看了 grpc-go issues1、grpc-go issues2 以及 HTTP2文档 才发现, 由于HTTP2有多路复用的特性, 对于同一个sever, 只需要维护一个连接就好了, 没有必要用多个连接去并行复用数据流. 连接数量减少对提升 HTTPS 部署的性能来说是一项特别重要的功能:可以减少开销较大的 TLS 连接数、提升会话重用率,以及从整体上减少所需的客户端和服务器资源。
]]>if err!=nil
, 但是如果想偷懒, 少带了上下文信息, 直接写 if err!=nil { return err}
或者 fmt.Errorf
携带的上下文信息太少了的话, 看到错误日志也会一脸懵逼, 难以定位问题.errors.New(str string)
定义错误常量, 让调用方去判断返回的 err
是否等于这个常量, 来进行区分处理;fmt.Errorf(fmt string, args... interface{})
增加一些上下文信息, 用文字的方式告诉调用方哪里出错了, 让调用方打错误日志出来;我最开始最常用的做法是, 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 |
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 |
以上包不满足要求, 只能造轮子了. 两个思想. API要设计的简单, 调用栈要好看 https://github.com/hanjm/errors
errors.New
函数, 兼容标准库的函数, 兼容很重要; 包装error的只有 errors.Errorf
函数, 只在最早出错的时候取调用栈, 调用方再包装时无需取调用栈, 此时只需要pc, 不需要这时就把文件名行号取出来; 取最里层的 error 只有 errors.GetInnerMost
, 用于和 error 常量比较或类型断言成自定义 struct type分类处理.在IDE中加个live template, 写errf回车就补全到if err!=nil {
err = errors.Errorf(err,"{{光标}}")
return
}
然后补充必要的注释和参数就行了, 在本地环境调试时看到错误日志点击就可以定位到源码, 在非本地环境跑看到错误日志相比之前也能更好地知道发生了什么, 复制文件名:行号到IDE中就能定位到源码, 大大减轻了错误处理的繁琐.
]]>NATS Server是一个高性能的, cloud native的, 基于发布订阅机制的消息系统, 没有消息持久化功能.
NATS Streaming Server是基于NATS Server的, 增加消息持久化功能的消息系统.
官网的文档并不详细, 很多重要的技术细节没说, 看了官网的文档之后发现用法很简单, 然后直接去写代码, 写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. 代码也值得阅读研究.
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 { |
使用.来分隔subject的级别. NATS允许subject包含斜杠/符号, 但NATS Streaming不允许, 因为NATS Streaming持久化时会使用subject名字来作为文件夹名,
gnatsd -sl reload
即可.-V
参数了解NATS, 生产环境就没必要了, 否则会把发过来的消息全打在日志里.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 |
很有用的Go风格的可选参数设计模式, 很多地方见过.
// Option is a function on the options for a connection. |
使用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) |
时间包括时间值和时区, 没有包含时区信息的时间是不完整的、有歧义的. 和外界传递或解析时间数据时, 应当像HTTP协议或unix-timestamp那样, 使用没有时区歧义的格式, 如果使用某些没有包含时区的非标准的时间表示格式(如yyyy-mm-dd HH:MM:SS), 是有隐患的, 因为解析时会使用场景的默认设置, 如系统时区, 数据库默认时区可能引发事故. 确保服务器系统、数据库、应用程序使用统一的时区, 如果因为一些历史原因, 应用程序各自保持着不同时区, 那么编程时要小心检查代码, 知道时间数据在使用不同时区的程序之间交换时的行为. 第三节会详细解释go程序在不同场景下time.Time的行为.
go1.9之前, time.Time的定义为
type Time struct { |
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. |
构造时间-获取现在时间-time.Now(), time.Now()使用本地时间, time.Local即本地时区, 取决于运行的系统环境设置, 优先取”TZ”这个环境变量, 然后取/etc/localtime, 都取不到就用UTC兜底.
func Now() Time { |
构造时间-获取某一时区的现在时间-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. |
构造时间-手动构造时间-time.Date(), 传入年元日时分秒纳秒和时区变量Location构造一个时间. 得到的是指定location的时间.
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time { |
序列化反序列化时间-文本和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 |
序列化反序列化时间-HTTP协议中的date, 统一GMT, 代码位于net/http/server.go:878
// TimeFormat is the time format to use when generating times in HTTP |
序列化反序列化时间-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. |
序列化反序列化时间-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 |
解析SQL语句中time.Time类型的参数的代码位于mysqlConn.interpolateParams方法https://github.com/go-sql-driver/mysql/blob/master/connection.go#L230-L273
case time.Time: |
从MySQL数据流中解析时间的代码位于textRows.readRow方法https://github.com/go-sql-driver/mysql/blob/master/packets.go#L772-L777, 注意只要MySQL连接字符串设置了parseTime=true, 就会解析时间, 不管你是用string还是time.Time接收的.
if !isNull { |
有个服务频繁使用最新汇率, 所以缓存了最新汇率对象, 汇率对象的过期时间设为第二天北京时间零点, 汇率过期则从数据库中去最新汇率, 设置过期时间的代码如下:
var startTime string = time.Now().UTC().Add(8 * time.Hour).Format("2006-01-02") |
这段代码使用了time.Parse, 如果时间格式中没有指定时区, 那么会得到使用本地时区下的第二天零点, 服务器时区设置为UTC0, 于是汇率缓存在UTC零点即北京时间八点才更新.
公共库中有一个GetBjTime()方法, 注释写着将服务器UTC转成北京时间, 代码如下
// 原版 |
同事用这个方法将得到的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更能体现本意吧.
协调世界时(英语: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时间,或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从协调世界时1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒[1]。 在多数Unix系统上Unix时间可以通过date +%s指令来检查。
时区列表
先看两个case
// case1: 135.90*100 ==== |
浮点数在单精度下, 135.9*100即出现了偏差, 双精度下结果正确.
// case2: 0.1 add 10 times === |
0.1加10次, 这下无论是float32和float64都出现了偏差.
为什么呢, Go和大多数语言一样, 使用标准的IEEE754表示浮点数, 0.1使用二进制表示结果是一个无限循环数, 只能舍入后表示, 累加10次之后就会出现偏差.
此外, 还有几个隐藏的坑https://play.golang.org/p/bQPbirROmN
package main |
##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. |
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. |
看看这两种decimal类型在文首的两个case下的结果, 同时跑个分.
main_test.go
package main |
BenchmarkShopspringDecimalCase1-8 2000000 664 ns/op 340 B/op 10 allocs/op |
可见两种实现在上面两个case下表示准确, TiDB的decimal实现的性能高于shopspring的实现, 堆内存分配次数也更少.
##4. MyDecimal的已知问题
用了一段时间后, tidb.MyDecimal也有一些问题
warning: Error disabling address space randomization: Operation not permitted |
在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). |
]]>$ docker run –device=/dev/snd:/dev/snd …
[]struct
还是[]*struct
make([]struct,0)
后append 还是 用sync.Pool
make([]struct,100)
写段代码跑个分, 结论是
[]*struct
的要比[]struct
多n次取指针的内存分配, 所有更慢, 如果不用修改结构体元素内的值, 没有必要用指针切片[]*struct
要比[]struct
慢sync.Pool
效果明显benchmark结果
BenchmarkStructSliceWithoutPool-8 200000 5458 ns/op 16320 B/op 8 allocs/op |
benchmark代码
package main |
nsq的官方文档的Dsign中提到一个PPThttps://speakerdeck.com/snakes/nsq-nyc-golang-meetup, 里面有这样一段话
总结一下.
go func()
简单好用, 创建开销也很小, 但也是有开销的. 很多情况下开固定数量worker, 用channel传递数据, 效果会更好.注意一个问题, go里面一个goroutine panic了, 会导致进程退出, 所以go func()
时第一行带上
go func(){ |
是安全的做法, worker channel法时类似
package main |
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是带缓冲的, 那么可以读到所有数据)
Python有import this
的zen of Python, 想不到Go也有箴言
https://speakerdeck.com/ajstarks/go-proverbs
log.SetOutput(w io.Writer)
来改变位置, w是一个实现了Write(p []byte) (n int, err error)
方法的io.Writer即可.package zaplog |
edit: ~/.zshrc line: plugins=(git python z tmux)
), z 文件夹名
就可以跳转到常用目录中最符合输入文件夹名的文件夹中, 非常方便, GitHub地址https://github.com/robbyrussell/oh-my-zshprivoxy. 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
, 局域网内其他设备也可以走此代理
listen-address 0.0.0.0:8118 |
然后在.zshrc或.bashrc中加入一下命令就可以通过proxy
dproxy
来切换是否在本终端下使用代理了.
function proxy(){ |
kcptun. bandwagon上的shadowsocks越来越慢, 不用kcptun加速没法正常使用, 只能不太厚道地超量发包了. GitHub地址https://github.com/xtaci/kcptun
--restart always
的container或docker swarm service 跑, docker启动时会自动启动这些container/service) GitHub地址https://github.com/jimbojsb/launchrocket brew install
简直不能太爽. 官网https://brew.sh/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
, 局域网内其他设备也可以走此代理
listen-address 0.0.0.0:8118 |
然后在.zshrc或.bashrc中加入一下命令就可以通过proxy
dproxy
来切换是否在本终端下使用代理了.
function proxy(){ |
proxier 可以按进程名指定哪些进程走ss代理, 比如ssh
edit: ~/.zshrc line: plugins=(git python z tmux)
), z 文件夹名
就可以跳转到常用目录中最符合输入文件夹名的文件夹中, 非常方便, GitHub地址https://github.com/robbyrussell/oh-my-zshbrew install aria2
即可. GitHub地址:https://github.com/aria2/aria2 GUI Client:https://github.com/yangshun1029/aria2gui主要语言栈: Python Java
演讲:
http://www.infoq.com/cn/news/2014/12/zhihu-architecture-evolution
主要语言栈: Python Java Go
演讲:
开源项目:
主要语言栈: node.js Go
演讲:
开源项目:
主要语言栈: Go
主要语言栈: GO
开源项目:
https://github.com/davideuler/architecture.of.internet-product
]]>[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 |
找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)
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 |
output:
15fc7bb-1e67-11e7-b8a5-00163e008796 |