开源实践

这篇文章记录下我的第一次开源实践经历,希望可以对开源小白参与开源提供一些帮助。 ## 契机 我想要参与开源的想法已经存在很久了,经常混迹于各大开源社区查看issue,并了解了各种面向学生的开源活动(譬如开源之夏、GLCC等等)。但囿于难度,都没有真正的参与进去。 只零星给cloudwego撰写过一些文档,虽说撰写文档也算贡献,但代码贡献显然更能令我感到鼓舞。 今年的开源之夏报名的时候,我联系过curve社区的导师,虽说最后没有入选,但七月份curve社区组织自己的开源活动的时候,我却收到了邮件邀请。 不得不说,curve社区是一个对新人非常友好的、包容的并且活跃的社区。同时也让我明白了,有时候即使失败也不是一无所获。 收到了邀请后,我赶忙查看了社区给出的题目,题目方向众多数量不少,如前所述,并不简单,且我的相关背景知识很不完善,于是跟着curve的roadmap开始学习分布式系统、分布式存储等相关知识。 短时间学完那么多知识并且应用于实践是比较困难的,并且等我准备好,相关的题目早已被选完(竞争激烈),因此我重新浏览题目,阅读源码,寻找适合自己参与的题目。 最终选择了[Fix request for time series data exceeding limit](https://github.com/opencurve/curve-manager/issues/3)这个题目。 ## 关于题目 [![pPLYbWV.png](https://z1.ax1x.com/2023/10/02/pPLYbWV.png)](https://imgse.com/i/pPLYbWV) 题目的意思大概是,有一些趋势图需要从start到end以一定的间隔从prometheus中取数据,但是prometheus有一个限制就是,它的查询只支持11000个点。你要做的是检查查询的点,并且当限制被超出时分裂请求。 prometheus是干什么的?start与end又是什么?什么是点? 带着这些疑问,我进行了一波搜索。 ### prometheus prometheus是一个开源项目,托管在github上,已经50k+star了,是广泛使用的开源项目,官方给出的描述如下: > Prometheus, a Cloud Native Computing Foundation project, is a systems and service monitoring system. It collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts when specified conditions are observed. 可以看到prometheus是一个系统和服务监控系统。它从配置的目标以给定的间隔收集metrics。它支持多维数据模型(由metric名字和键值对组成的时间序列数据)。 [![pPLtD6U.png](https://z1.ax1x.com/2023/10/02/pPLtD6U.png)](https://imgse.com/i/pPLtD6U) 通过阅读prometheus源码得知,11000这个数是(end-start)/step得到的。 ### 题目解析 既然prometheus以时间间隔收集序列数据metrics。那么题目中的start和end也就是时间段的开始和结束时间咯,并且11000也就是一段时间范围内时间的间隔数量,题目中的“点”对应的应该就是数据的采集时间节点。 我理解了题目:**有时为了做趋势图,需要有很多数据点,这个数据从prometheus中拿到。但是prometheus一次查询只能查11000个点的数据,我们在从prometheus中查询数据的时候,如果超过了11000个点就会报错。所以需要我们在查询时判断下,如果超过11000个点,就修改请求多次查询,这样不会报错也查到了数据。** ### 关于curve-manager 我要解决的这个issue属于[curve-manager](https://github.com/opencurve/curve-manager)项目。 curve-manager提供基于Web的集群管理能力,使得存储集群的部署和管理门槛进相对 CLI方式一步降低,提供一种更加清晰直观的视图。其架构如下: [![pPLtv1f.png](https://z1.ax1x.com/2023/10/02/pPLtv1f.png)](https://imgse.com/i/pPLtv1f) prometheus监控Curve集群的状态并获得监控数据。 ## 阅读源码 了解了题目的意图之后,就该阅读源码寻找应该在何处下手了。 对于刚接触项目的人来说,项目代码繁杂不知从哪入手,参与开源也会锻炼阅读源码的能力。 首先我应该找到项目中执行从prometheus获取数据的代码段,很惊喜地发现/internal下的一个名为metrics的文件夹,联想到prometheus采集出来的数据为metrics,相关代码可能就此处。 查看/internal/metrics/bsmetric文件夹,发现有monitor.go文件,监控集群状态的代码段应该就在此处,monitor.go中有一个函数名为GetClusterSpace,代码如下: ```go func GetClusterSpace(start, end, interval uint64) ([]metricomm.SpaceTrend, error) { spaces := []metricomm.SpaceTrend{} retMap := make(map[float64]*metricomm.SpaceTrend) // total, alloc requestSize := 2 results := make(chan metricomm.MetricResult, requestSize) totalName := fmt.Sprintf("%s&start=%d&end=%d&step=%d", CLUSTER_LOGICAL_CAPACITY, start, end, interval) usedName := fmt.Sprintf("%s&start=%d&end=%d&step=%d", CLUSTER_LOGICAL_ALLOC, start, end, interval) go metricomm.QueryRangeMetric(totalName, &results) go metricomm.QueryRangeMetric(usedName, &results) count := 0 for res := range results { if res.Err != nil { return nil, res.Err } ret := metricomm.ParseMatrixMetric(res.Result.(*metricomm.QueryResponseOfMatrix), metricomm.INSTANCE) if res.Key.(string) == totalName { for _, v := range ret { for _, item := range v { total, e := strconv.ParseUint(item.Value, 10, 64) if e != nil { return nil, e } if _, ok := retMap[item.Timestamp]; ok { retMap[item.Timestamp].Total = total / common.GiB } else { retMap[item.Timestamp] = &metricomm.SpaceTrend{ Timestamp: item.Timestamp, Total: total / common.GiB, } } } } } else if res.Key.(string) == usedName { for _, v := range ret { for _, item := range v { used, e := strconv.ParseUint(item.Value, 10, 64) if e != nil { return nil, e } if _, ok := retMap[item.Timestamp]; ok { retMap[item.Timestamp].Used = used / common.GiB } else { retMap[item.Timestamp] = &metricomm.SpaceTrend{ Timestamp: item.Timestamp, Used: used / common.GiB, } } } } } count += 1 if count >= requestSize { break } } for _, v := range retMap { spaces = append(spaces, *v) } return spaces, nil } ``` 虽然对代码的细节还不甚了解,但是看其入参start、end和interval,以及创建的goroutine: go metricomm.QueryRangeMetric(totalName, &results),像啊很像啊,这不就是从什么地方进行范围查询吗? 接着去/internal/metrics/common中看QueryRangeMetric的实现,代码如下: ```go func QueryRangeMetric(name string, results *chan MetricResult) { var res QueryResponseOfMatrix err := core.GMetricClient.GetMetricFromPrometheus( RANGE_METRIC_PATH, VECTOR_METRIC_QUERY_KEY, name, &res) *results <- MetricResult{ Key: name, Err: err, Result: &res, } } ``` 它的入参name是包含了start、end和interval信息的,它调用了一个叫做GetMetricFromPrometheus的函数,并传入了name作为参数。 Prometheus终于出现了!从函数名来看已经很明显了,从Prometheus获取Metric,这个函数就是我们要找的函数。 ### GetMetricFromPrometheus函数 接着去看/internal/metrics/core/metrics.go中GetMetricFromPrometheus的实现,代码如下: ```go func (cli *metricClient) GetMetricFromPrometheus(path, queryKey, queryValue string, ret interface{}) error { url := (&url.URL{ Scheme: "http", Host: cli.PromeAddr, Path: path, RawQuery: fmt.Sprintf("%s=%s", queryKey, queryValue), }).String() resp, err := cli.client.R(). SetHeader("Connection", "Keep-Alive"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", "Curve-Manager"). SetResult(ret). Execute("GET", url) if err != nil { return fmt.Errorf("get prometheus metric failed: %v", err) } else if resp.StatusCode() != 200 { return fmt.Errorf("get prometheus metric failed, status = %s, url = %v", resp.Status(), url) } return nil } ``` GetMetricFromPrometheus()是metricClient结构体的一个方法。 metricClient结构体: ```go type metricClient struct { client *resty.Client PromeAddr string EtcdAddr []string MdsDummyAddr []string SnapShotCloneServerDummyAddr []string } ``` 其中client是一个HTTP客户端。 采用HTTP API的方式向Prometheus发送请求得到响应,Prometheus官方文档中对应于范围查询的HTTP API为: ```bash GET /api/v1/query_range POST /api/v1/query_range ``` #### 参数 path:QueryRangeMetric给它传入的path为RANGE_METRIC_PATH,值为"/api/v1/query_range",是Prometheus的HTTP API queryKey: QueryRangeMetric给它传入的queryKey为VECTOR_METRIC_QUERY_KEY,其值为"query" queryValue: 就是QueryRangeMetric函数接收到的name值,是一个字符串,其中包括了start、end、interval和想要查询的数据类型信息。 ret:ret用于接收Prometheus的响应结果 #### 代码逻辑 使用HTTP客户端向Prometheus发送GET请求,指定查询的数据、查询范围和间隔,并把结果放在ret中。 好了,了解到这个程度,我认为可以考虑解决这个issue的代码实现了。 ## 代码实现 我已经比较清楚,应该判断(end-start)/interval的大小,若大于11000则做多次请求,合并结果之后再返回结果。 但是涉及到这个查询操作的函数有三个,即GetClusterSpace、QueryRangeMetric和GetMetricFromPrometheus。应该在哪里实现上面的逻辑呢? 将一个请求划分为多个请求是简单的,无非是多调用几次函数,但是将结果合并就相比之下复杂了些,因此我应该先分析下查询结果的数据流向,判断在何处合并查询结果比较合适。 ### 查询结果的数据流 GetMetricFromPrometheus()是第一个获取到查询结果的函数,它将结果放在了interface{}类型的ret中。 QueryRangeMetric()给GetMetricFromPrometheus()传入的ret是QueryResponseOfMatrix类型的,随后被放入了QueryResult类型的结构体MetricResult中。 至此,一次查询返回的数据就成型了。GetClusterSpace()涉及到具体的业务了,本着降低耦合度的原则,不应该在此处实现上面的逻辑。 GetMetricFromPrometheus()处得到的结果,是一个interface{}类型的数据,并不好合并结果。因此在QueryRangeMetric()中实现请求分片和请求结果合并是合理的也是好实现的。 ### 实现 遇到了新的问题,在Prometheus的代码中,使用(end-start)/step来判断点的数量,但是这里的start并不是GetClusterSpace()函数参数中的start,不可以简单地(end-start)/interval来计算,我需要研究一下,GetClusterSpace()中的start是如何转变成为Prometheus代码中的start的。 [![pPLtD6U.png](https://z1.ax1x.com/2023/10/02/pPLtD6U.png)](https://imgse.com/i/pPLtD6U)

版权声明: 如无特别声明,本文版权归 月梦の技术博客 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 记第一次开源经历(To Curve社区) 》

本文链接:https://ymiir.netlify.app//draft/2023-10-04-%E8%AE%B0%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%BC%80%E6%BA%90%E7%BB%8F%E5%8E%86(To%20Curve)

本文最后一次更新为 天前,文章中的某些内容可能已过时!