小厂内部私有Go module拉取方案(续)

本文永久链接 – https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2

自从去年在公司搭建了内部私有Go module proxy后,我们的私有代理工作得基本良好。按理说,这篇续篇本不该存在:)。

日子一天天过去,Go团队逐渐壮大,空气中都充满了“Go的香气”。

突然有一天,业务线考虑将目前在用的gerrit换成gitlab。最初使用gerrit的原因不得而知,但我猜是想使用gerrit强大且独特的code review机制和相应的工作流。不过由于业务需求变化太快,每个迭代的功能都很多,“+2”的review机制到后来就形同虚设了。

如果不用gerrit review工作流,那么gerrit还有什么存在的价值呢。从管理员那边反馈,gerrit配置起来也是比较复杂的,尤其是权限。两者叠加就有了迁移到gitlab的想法。这样摆在Go团队面前的一个事情就是如何让我们内部私有go module代理适配gitlab

如果你还不清楚我们搭建私有Go module代理的原理,那么在进一步往下阅读前,请先阅读一下《小厂内部私有Go module拉取方案》

适配gitlab

回顾一下我们的私有Go module代理的原理图:

基于这张原理图,我们分析后得出结论:要适配gitlab仓库,其实很简单,只需修改govanityurls的配置文件中的各个module的真实repo地址即可,这也符合更换一个后端代码仓库服务理论上开发人员无感的原则。

下面我们在gitlab上创建一个foo repo,其对应的module path为mycompany.com/go/foo。我们使用ssh方式拉取gitlab repo,先将goproxy所在主机的公钥添加到gitlab ssh key中。然后将gitlab clone按钮提示框中给出的clone地址:git@10.10.30.30:go/foo.git填到vanity.yaml文件中:

//vanity.yaml
  ... ...
  /go/foo:
     repo: ssh://git@10.10.30.30:go/foo.git
     vcs: git

我门在一台开发机上建立测试程序,该程序导入mycompany.com/go/foo,执行go mod tidy命令的结果如下:

$go mod tidy
go: finding module for package mycompany.com/go/foo
demo imports
    mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/foo@latest:
    go: mycompany.com/go/foo@latest: unrecognized import path "mycompany.com/go/foo": http://mycompany.com/go/foo?go-get=1: invalid repo root "ssh://git@10.10.30.30:go/foo.git": parse "ssh://git@10.10.30.30:go/foo.git": invalid port ":go" after host

从goproxy返回的response内容来看,似乎是goproxy使用的go命令无法识别:”ssh://git@10.10.30.30:go/foo.git”,认为10.10.30.30后面的分号后面应该接一个端口,而不是go。

我们将repo换成下面这样的格式:

  /go/foo:
     repo: ssh://git@10.10.30.30:80/go/foo.git
     vcs: git

重启govanityurls并重新执行go mod tidy,依旧报错:

$go mod tidy
go: finding module for package mycompany.com/go/foo
demo imports
    mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/foo@latest:
    go: module mycompany.com/go/foo: git ls-remote -q origin in /root/.bin/goproxycache/pkg/mod/cache/vcs/4d37c02c151342112bd2d7e6cf9c0508b31b8fe1cf27063da6774aa0f53d872f: exit status 128:
        kex_exchange_identification: Connection closed by remote host
        fatal: Could not read from remote repository.

直接在主机上通过git clone git@10.10.30.30:80/go/foo.git也是报错的!ssh不行,我们再来试试http方式。 使用http方式呢,每次clone都需要输入用户名密码,不适合goproxy。是时候让personal token上阵了!在gitlab上分配好personal token,然后在本地建立~/.netrc如下:

# cat ~/.netrc
machine 10.10.30.30
login tonybai
password [your personal token]

然后我们将vanity.yaml中的repo改为如下形式:

// vanity.yaml

  /go/foo:
     repo: http://10.10.30.30/go/foo.git
     vcs: git

这样再执行go mod tidy,foo仓库就被顺利拉取了下来。

答疑

1. git clone错误

在搭建goproxy时,我们通常会在goproxy服务器上手工验证一下是否可以通过git成功拉取私有仓库,如果git clone出现下面错误信息,是什么问题呢?

$ git clone ssh://tonybai@10.10.30.30:29418/go/common
Cloning into 'common'...
Unable to negotiate with 10.10.30.30 port 29418: no matching key exchange method found. Their offer: diffie-hellman-group14-sha1,diffie-hellman-group1-sha1
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

这里的错误提示信息其实是很清楚明了的。git服务器端支持diffie-hellman-group1-sha1和diffie-hellman-group14-sha1这两种密钥交换方法,而git客户端却默认一个都不支持。

怎么解决呢?我们需要在goproxy所在主机增加一个配置.ssh/config:

// ~/.ssh/config
Host 10.10.30.30
    HostName 10.10.30.30
    User tonybai
    Port 29418
    KexAlgorithms +diffie-hellman-group1-sha1

    IdentityFile ~/.ssh/id_rsa

有了这条配置后,我们就可以成功clone。

2. 使用非安全连接

有些童鞋使用这个方案后会遇到下面问题:

$go get mycompany.com/go/common@latest
go: module mycompany.com/go/common: reading http://10.10.30.30:10000/mycompany.com/go/common/@v/list: 404 Not Found
    server response:
    go list -m -json -versions mycompany.com/go/common@latest:
    go list -m: mycompany.com/go/common@latest: unrecognized import path "mycompany.com/go/common": https fetch: Get "https://mycompany.com/go/common?go-get=1": dial tcp 127.0.0.1:443: connect: connection refused

首先,go get得到的服务端响应信息中提示:无法连接127.0.0.1:443,查看goproxy主机的nginx access.log,也无日志。说明goproxy没有发起请求。也就是说问题出在go list命令这块,它为什么要去连127.0.0.1:443?我们的代码服务器使用的可是http而非https方式访问。

这让我想起了Go 1.14中增加的GOINSECURE,go命令默认采用的是secure方式,即https去访问代码仓库的。如果不要求非得以https获取module,或者即便使用https,也不再对server证书进行校验,那么需要设置GOINSECURE环境变量,比如;

export GOINSECURE="mycompany.com"

这样再获取mycompany.com/…下面的go module时,就不会出现上面的错误了!


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Prometheus采不到数据了!居然是Prometheus client包的锅

本文永久链接 – https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package

在基于eBPF的新一代观测设施尚未成熟之前,我们采用了业界成熟的Prometheus+Grafana方案采集节点与应用度量指标(metrics)信息。众所周知,这样的方案是一种对应用有侵入的方案,即需要在应用内部嵌入采集度量信息及与Prometheus通信的client包。

Prometheus官方提供并维护了主流语言的client包,包括Go、Java、Python、Ruby、Rust等,如下图:

Prometheus的go client端使用起来也不算复杂,总共分两步:

  • 把你要获取的度量指标注册(Register)到Prometheus的Registry中;
  • 建立起一个HTTP Server,暴露度量指标采集端口即可。

Prometheus采用拉模型(pull)收集时序度量数据,数据拉取行为是由Prometheus服务端来决定的,比如可以设定Prometheus拉取各个采集点的时间周期。 一般来说,这个技术栈已经很成熟,配置完启动后,马上就能看到效果。这个技术栈也很稳定,我们使用后一直运行良好,直到本周压测时遇到一个问题:Prometheus采不到数据了

从最初的数据由连续的线变成“断断续续”的点,见下图:

到后来干脆就无法采到任何数据了:

之前Prometheus跑的好好的,为什么现在却采不到数据了呢?这次与之前的不同之处在于我们的压测用例情景下,每个服务节点都要建立百万以上的连接,而之前仅仅是10w左右的量级。

好在我们部署了在线Continuous Profiling工具,可以查看一下压测那段时间的资源占用,如下图:

上面是一个alloc object的火焰图,我们看到Prometheus client的Registry.Gather方法占了50%的内存分配开销,这是很不正常的。继续沿着Gather函数的火焰图看,我们看到底端居然是readdir。我们应用注册的度量指标也没有哪个采集时需要readdir啊!

要想解决这个问题只有翻Prometheus client源码了!

我们使用的是prometheus client端的默认defaultRegistry。 从源码中可以看到:这个defaultRegistry在初始化时,默认注册了两个collector:

// registry.go
func init() {
    MustRegister(NewProcessCollector(ProcessCollectorOpts{}))
    MustRegister(NewGoCollector())
}

我们发现:第一个processCollector会采集如下度量指标数据:

// process_collector.go
func (c *processCollector) Describe(ch chan<- *Desc) {
    ch <- c.cpuTotal
    ch <- c.openFDs
    ch <- c.maxFDs
    ch <- c.vsize
    ch <- c.maxVsize
    ch <- c.rss
    ch <- c.startTime
}

在采集openFDs时,processCollector遍历了/proc/{pid}下面的fd目录:

// process_collector_other.go
func (c *processCollector) processCollect(ch chan<- Metric) {
    pid, err := c.pidFn()
    if err != nil {
        c.reportError(ch, nil, err)
        return
    }

    p, err := procfs.NewProc(pid)
    if err != nil {
        c.reportError(ch, nil, err)
        return
    }

    if stat, err := p.Stat(); err == nil {
        ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
        ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
        ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
        if startTime, err := stat.StartTime(); err == nil {
            ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
        } else {
            c.reportError(ch, c.startTime, err)
        }
    } else {
        c.reportError(ch, nil, err)
    }

    if fds, err := p.FileDescriptorsLen(); err == nil { // 这里获取openFDs
        ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
    } else {
        c.reportError(ch, c.openFDs, err)
    }

    if limits, err := p.Limits(); err == nil {
        ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
        ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
    } else {
        c.reportError(ch, nil, err)
    }
}

采集openFDS时,processCollector调用了FileDescriptorsLen方法,在FileDescriptorsLen方法调用的fileDescriptors方法中,我们找到了对Readdirnames的调用,见下面源码片段:

// github.com/prometheus/procfs/proc.go

// FileDescriptorsLen returns the number of currently open file descriptors of
// a process.
func (p Proc) FileDescriptorsLen() (int, error) {
    fds, err := p.fileDescriptors()
    if err != nil {
        return 0, err
    }

    return len(fds), nil
}  

func (p Proc) fileDescriptors() ([]string, error) {
    d, err := os.Open(p.path("fd"))
    if err != nil {
        return nil, err
    }
    defer d.Close()

    names, err := d.Readdirnames(-1) // 在这里遍历目录中的文件
    if err != nil {
        return nil, fmt.Errorf("could not read %q: %w", d.Name(), err)
    }

    return names, nil
}

通常情况下,读取/proc/{pid}/fd目录是没有问题的,但当我们的程序上连接着100w+的连接时,意味着fd目录下有100w+的文件,逐一遍历这些文件将带来很大的开销。这就是导致Prometheus在超时时间(通常是10几秒)内无法及时采集到数据的原因。

那么如何解决这个问题呢?

临时解决方法是将registry.go文件的init函数中的MustRegister(NewProcessCollector(ProcessCollectorOpts{}))这一行注释掉!这个进程度量指标信息对我们用处不大。不过这样做的不足是我们自己需要维护一份prometheus golang client包,需要用到go mod replace,十分不便,并且不便于prometheus golang client包的版本升级。

一劳永逸的解决方法是:不使用默认Registry,而是使用NewRegistry函数新建一个Registry。这样我们抛开默认注册的那些度量指标,并可以自行定义我们要注册的度量指标。需要时,我们也可以将ProcessCollector加进来,这个根据不同Go程序的需要而定。

按这个方案修改后,那些熟悉的连续曲线就又重现在眼前了!


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats