Singleflight 介绍

缓存击穿 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多 key。 缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。 我在 go-redis/cache 中发现了库使用了 singleflight , 经过查阅资料,了解了 这个库的主要作用就是将一组相同的请求合并成一个请求,实际上只会去请求一次,然后对所有的请求返回相同的结果。这样会大大降低数据库的压力。 singleflight 使用 函数签名 type Group struct { mu sync.Mutex // protects m m map[string]*call // lazily initialized } // Do 执行函数,对同一个 key 多次调用的时候,在第一次调用没有执行完的时候 // 只会执行一次 fn 其他的调用会阻塞住等待这次调用返回 // v, err 是传入的 fn 的返回值 // shared 表示是否真正执行了 fn 返回的结果,还是返回的共享的结果 func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) // DoChan 和 Do 类似,只是 DoChan 返回一个 channel,也就是同步与异步的区别 func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result // Forget 用于通知 Group 删除某个 key 这样后面继续这个 key 的调用的时候就不会在阻塞等待了 func (g *Group) Forget(key string) 示例 接下来我们来讲解一个简单的例子,我们来看看 singleflight 的使用方式,先来看一个简单的例子: ...

六月 18, 2022 · overstarry

Go 定时监控 Https 证书

起因 最近有由于一个域名的 https 证书过期,导致某个网站出现大面积无法正常使用的故障。于是我打算使用 go 语言 来监控域名的 HTTPS 证书过期情况,来及时续期证书。 HTTPS 证书 了解证书加密体系的应该知道,TLS 证书是链式信任的,所以中间任何一个证书过期、失效都会导致整个信任链断裂,不过单纯的 Let’s Encrypt ACME 证书检测可能只关注末端证书即可,除非哪天 Let’s Encrypt 倒下… 解决 在 go 语言中,Go 在发送 HTTP 请求后,在响应体中会包含一个 TLS *tls.ConnectionState 结构体,该结构体中目前存放了服务端返回的整个证书链: // ConnectionState records basic TLS details about the connection. type ConnectionState struct { // Version is the TLS version used by the connection (e.g. VersionTLS12). Version uint16 // HandshakeComplete is true if the handshake has concluded. HandshakeComplete bool // DidResume is true if this connection was successfully resumed from a // previous session with a session ticket or similar mechanism. DidResume bool // CipherSuite is the cipher suite negotiated for the connection (e.g. // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_AES_128_GCM_SHA256). CipherSuite uint16 // NegotiatedProtocol is the application protocol negotiated with ALPN. NegotiatedProtocol string // NegotiatedProtocolIsMutual used to indicate a mutual NPN negotiation. // // Deprecated: this value is always true. NegotiatedProtocolIsMutual bool // ServerName is the value of the Server Name Indication extension sent by // the client. It's available both on the server and on the client side. ServerName string // PeerCertificates are the parsed certificates sent by the peer, in the // order in which they were sent. The first element is the leaf certificate // that the connection is verified against. // // On the client side, it can't be empty. On the server side, it can be // empty if Config.ClientAuth is not RequireAnyClientCert or // RequireAndVerifyClientCert. PeerCertificates []*x509.Certificate // VerifiedChains is a list of one or more chains where the first element is // PeerCertificates[0] and the last element is from Config.RootCAs (on the // client side) or Config.ClientCAs (on the server side). // // On the client side, it's set if Config.InsecureSkipVerify is false. On // the server side, it's set if Config.ClientAuth is VerifyClientCertIfGiven // (and the peer provided a certificate) or RequireAndVerifyClientCert. VerifiedChains [][]*x509.Certificate // SignedCertificateTimestamps is a list of SCTs provided by the peer // through the TLS handshake for the leaf certificate, if any. SignedCertificateTimestamps [][]byte // OCSPResponse is a stapled Online Certificate Status Protocol (OCSP) // response provided by the peer for the leaf certificate, if any. OCSPResponse []byte // TLSUnique contains the "tls-unique" channel binding value (see RFC 5929, // Section 3). This value will be nil for TLS 1.3 connections and for all // resumed connections. // // Deprecated: there are conditions in which this value might not be unique // to a connection. See the Security Considerations sections of RFC 5705 and // RFC 7627, and https://mitls.org/pages/attacks/3SHAKE#channelbindings. TLSUnique []byte // ekm is a closure exposed via ExportKeyingMaterial. ekm func(label string, context []byte, length int) ([]byte, error) } 可以看到 PeerCertificates 包含了所有的证书,我们只只要遍历 PeerCertificates,根据 NotBefore, NotAfter 字段就能进行是否过期的判断 ...

六月 11, 2022 · overstarry

Go 并发 Sync.Once 解析

在 go 语言中我们可以使用 sync.Once 对象来实现函数方法只执行一次的功能。 简单代码示例 package main import ( "fmt" "sync" ) func main() { var ( o sync.Once wg sync.WaitGroup ) for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() o.Do(func() { fmt.Printf("hello %d\n", i) }) }(i) } wg.Wait() } 输出: hello 9 不使用 Sync.Once 的结果如下: hello 9 hello 4 hello 0 hello 1 hello 2 hello 3 hello 6 hello 5 hello 7 hello 8 可以看到,在使用 sync.Once 的情况下,只执行一次函数。 ...

六月 3, 2022 · overstarry

Go 截取视频某一帧图片

前言 最近遇到一个需求,需要截取视频的某一帧图片作为视频封面。我搜寻了相关资料,在 go 语言端常见的有两种做法,1)使用 opencv 的 go 绑定库,2)使用 ffmpeg 的 go 绑定库。 这里我打算使用第二种方法,使用 ffmpeg 的 go 绑定库。 ffmpeg 介绍 FFmpeg 是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用 LGPL 或 GPL 许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库 libavcodec,为了保证高可移植性和编解码质量,libavcodec 里很多 codec 都是从头开发的。 FFmpeg 在 Linux 平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括 Windows、Mac OS X 等。 FFmpeg 项目由以下几部分组成: FFMpeg 视频文件转换命令行工具,也支持经过实时电视卡抓取和编码成视频文件。 FFServer 基于 HTTP(RTSP 正在开发中) 用于实时广播的多媒体服务器,也支持时间平移. FFplay 用 SDL 和 FFmpeg 库开发的一个简单的媒体播放器. libavcodec 一个包含了所有 FFmpeg 音视频编解码器的库。为了保证最优性能和高可复用性,大多数编解码器从头开发的. libavformat 一个包含了所有的普通音视格式的解析器和产生器的库 解决 1 安装 ffmpeg 浏览器访问 https://ffbinaries.com/downloads 根据你的系统安装 ffmpeg 2 安装 ffmpeg 的 go 绑定库 ...

五月 28, 2022 · overstarry

Go errgroup

我们知道在 go 语言中很容易开启携程进行并发任务,但是如何处理并发过程中的错误是非常棘手的,接下来我就来介绍 errgroup 的用法。 errgroup errgroup 包里主要是一个结构体和相应的方法,它可以让你在并发任务中处理错误。 type Group struct { // context 的 cancel 方法 cancel func() // 复用 WaitGroup wg sync.WaitGroup sem chan token // 用来保证只会接受一次错误 errOnce sync.Once // 保存第一个返回的错误 err error } func WithContext(ctx context.Context) (*Group, context.Context) func (g *Group) done() func (g *Group) Wait() error func (g *Group) Go(f func() error) func (g *Group) TryGo(f func() error) bool func (g *Group) SetLimit(n int) 通过 WithContext 可以创建一个可以取消的 Group,当然除此之外也可以零值的 Group 也可以直接使用,但是出错之后就不会取消其他的 goroutine 了。Go 方法 传入一个函数参数,会启动一个 goroutine 处理。 Wait 类似 WaitGroup 的 Wait 方法,等待所有的 goroutine 结束后退出,返回的错误是一个出错的 err。 TryGo 是和 SetLimit 配套的,只有当 group 中的 goroutines 数量小于配置的数量时,才会在 goroutine 中调用函数。TryGo 用来判断 goroutine 是否启动。 ...

五月 21, 2022 · overstarry

应用内存升高原因排查

起因 最近一个部署了 go 应用的服务器出现了 OOM 的现象,内存占用过高。 原因 通过 Pyroscope 分析得出是因为 Minio 的 go sdk 中的 PutObject 函数占用了大量的内存。Pyroscope 是什么,前面的文章已经介绍过了,这里就不过多介绍了。 接下来我们通过查看相关的源码来查看是什么原因。 // PutObject creates an object in a bucket. // // You must have WRITE permissions on a bucket to create an object. // // - For size smaller than 16MiB PutObject automatically does a // single atomic PUT operation. // // - For size larger than 16MiB PutObject automatically does a // multipart upload operation. // // - For size input as -1 PutObject does a multipart Put operation // until input stream reaches EOF. Maximum object size that can // be uploaded through this operation will be 5TiB. // // WARNING: Passing down '-1' will use memory and these cannot // be reused for best outcomes for PutObject(), pass the size always. // // NOTE: Upon errors during upload multipart operation is entirely aborted. func (c *Client) PutObject(ctx context.Context, bucketName, objectName string, reader io.Reader, objectSize int64, opts PutObjectOptions, ) (info UploadInfo, err error) { if objectSize < 0 && opts.DisableMultipart { return UploadInfo{}, errors.New("object size must be provided with disable multipart upload") } err = opts.validate() if err != nil { return UploadInfo{}, err } return c.putObjectCommon(ctx, bucketName, objectName, reader, objectSize, opts) } 从方法的注释可以看出,当传递的大小为 -1 时,会进行多次 put 操作,直到输入流结束。多次 put 操作的最大大小为 5TiB,并且不能重用内存,导致占用大量内存。 ...

五月 14, 2022 · overstarry

多平台博客发布工具 openwrite 使用

介绍 OpenWrite 是一款便捷的多平台博客发布工具,可以在 OpenWrite 编写 markdown 文档,然后发布到其他博客平台,目前已经支持 CSDN、SegmentFault、掘金、博客园、简书等知名平台。 使用 注册 进入 http://admin.openwrite.cn/, 注册并登陆账号。 安装浏览器插件 第一步需要安装浏览器插件,根据这个链接教程安装,只有安装了插件,才能配置分发的平台。根据对插件的源码的分析,插件是通过 cookie、storage 等浏览器数据进行认证登陆的。此插件还包括模拟写作和 mackdown 编辑器。 认证配置渠道 安装完插件后,点击渠道管理,可以看到可配置的平台,支持多个平台。相应的平台如果没有登陆则显示未登陆,这时需要你登陆相应的平台。登陆平台后,点击认证,即可配置渠道。 这里以掘金为例,认证成功后,可以进行相应的配置,配置文章分类和标题。 发布文章 点击文章管理界面,添加文章,添加文章后,可以选择配置的平台,并发布到相应的平台。 普通用户一个月只有 10 次机会,一个平台占用一次机会。

五月 7, 2022 · overstarry

Buf 初入门 2

上文讲了 Buf lint 命令的基础方法,本文将介绍 Buf lint 命令的一些常用配置。 如果你的项目中没有 buf.yaml 配置文件,Buf lint 会提供一个默认的配置文件,默认内容如下: version: v1 lint: use: - DEFAULT except: - FILE_LOWER_SNAKE_CASE ignore: - bat - ban/ban.proto ignore_only: ENUM_PASCAL_CASE: - foo/foo.proto - bar BASIC: - foo enum_zero_value_suffix: _UNSPECIFIED rpc_allow_same_request_response: false rpc_allow_google_protobuf_empty_requests: false rpc_allow_google_protobuf_empty_responses: false service_suffix: Service allow_comment_ignores: true 配置选项 接下来开始介绍 Buf lint 命令的配置选项。 use use 选项配置 lint 的类别,不同类别有相应的规则,有多种类别:DEFAULT、FILE_LOWER_SNAKE_CASE、BASIC 等。默认是 DEFAULT 类别。 except except 选项配置 lint 不使用的类别。 下面的配置,显示使用 DEFAULT 类别,但是不使用 ENUM_NO_ALLOW_ALIAS ,BASIC 类别中的规则。 version: v1 lint: use: - DEFAULT except: - ENUM_NO_ALLOW_ALIAS - BASIC ignore ignore 选项指定忽略的文件,可以是文件名,也可以是目录,注意路径是要相对于 buf.yaml 文件的。 ...

五月 4, 2022 · overstarry

Buf 初入门 1

Buf 的目标是将 API 开发转向模式驱动的范式,从而为未来铺平道路,使 API 以服务所有者和客户可以依赖的方式定义。 与简单地暴露 REST/JSON 服务相比,使用 IDL 来定义 API 有很多好处,今天,Protobuf 是业界最稳定、最广泛采用的 IDL。但就目前的情况来看,使用 Protobuf 比使用 JSON 作为数据传输格式要困难得多。 Buf 正在建立工具,使 Protobuf 对服务所有者和客户来说是可靠和友好的,同时保持它在技术上的明显优势。您的组织不需要重新发明轮子来高效地创建、维护和使用 Protobuf API。我们将为您处理您的 Protobuf 管理策略,因此您可以专注于重要的事情。 本篇文章是讲述 buf 使用的第一篇文章,主要讲解使用 buf 定义 proto 文件的 lint 规则。统一的 lint 规则,可以让个人或团队定义的 API 保持一致。 lint 1 创建 buf.yaml 文件 使用 buf mod init 命令创建 buf.yaml 文件。 version: v1 breaking: use: - FILE lint: use: - DEFAULT 使用默认 lint 规则。 2 运行 lint 命令 使用 buf lint 命令运行 lint。 ...

四月 23, 2022 · overstarry

Go 统计代码测试覆盖率

今天来讲一讲如何统计 go 代码的测试覆盖率,主要是 cover 命令。 cover 基本用法 1 先简单写个函数和相应的测试,代码如下: func Max(a, b int) int { if a > b { return a } return b } 这个函数就是简单的比较大小,如果 a > b,返回 a,否则返回 b。 测试代码如下 package main import "testing" func TestMax(t *testing.T) { type args struct { a int b int } tests := []struct { name string args args want int }{ { name: "a is larger than b", args: args{ a: 2, b: 1, }, want: 2, }, { name: "b is larger than a", args: args{ a: 1, b: 2, }, want: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Max(tt.args.a, tt.args.b); got != tt.want { t.Errorf("Max() = %v, want %v", got, tt.want) } }) } } 2 go test -cover 能够统计出代码测试的覆盖率,这是一种比统计函数是否被调用更强悍的手法。我们执行这个命令。输出如下: ...

四月 16, 2022 · overstarry