阅读视图

发现新文章,点击刷新页面。

Apache Pulsar 的社群指标

去年十一月,我成为了 Apache Pulsar 社群 Committers 的一员。

成为 Committer 之前和之后,我都积极参与了代码仓库上 Issue 和 Pull Request (PR) 的处理回应和评审。去年十二月期间,我把未解决的 Issue 和 PR 数量分别从接近 2000 个和 400 个,在动态增长的情况下缩小到了小于 1000 个和接近 200 个。

关于如何高效地、分门别类地处理社群积压的议题和补丁,我可能会用另一篇文章来讨论。本文想讲的是在这个过程里我产生的另一个想法,那就是如何分析在这类过程中产生的活动,从而判断社群的健康情况呢?

tisonkun activities in 2022

数据源与展示

要想做数据分析,首先应该确定使用什么数据源。不同数据源的数据质量和数据口径有很大的差距,可能导致同一个逻辑概念最终算出来的结果之间大相径庭。

我们的目标是分析社群成员的在 GitHub 平台上的活动指标和趋势,而 GitHub 本身提供了 Event 接口获取平台上的所有公开活动。

不过,这个接口返回的数据只有最近九十天的数据,要想获取完整的数据,就需要依靠一个从近十年前已经开始爬取 GitHub Events 数据并提供开放下载的项目 GH Archive 了。关于如何从这个数据集构建一个自有的数据源,可以参考《基于 ClickHouse 的 GitHub 事件数据库》这篇文章。

GH Archive 提供的是 GitHub 全平台所有活动的数据。由于 GitHub 日益活跃,这个数据集的总体大小在几个 TB 的量级。虽然对于一个具体项目社群比如 Pulsar 来说,筛选出与己相关的仓库的活动最终大约只有 MB 量级的数据,但是使用 GH Archive 的数据始终要过一遍全量的活动数据。

另一种方案是把 GH Archive 的工作也集成到数据源制作的工具集里,直接从 GitHub 的 Event 接口里仅拉取相关仓库的活动。这样做可行的原因是往往最近的数据才是最有价值的,一个仓库五年前是什么活跃水平,很大程度上只是一个图一乐的数字。不过,要想这么做,数据源生成的流水线也不简单,需要维护一个定期任务的调度,以及数据去重和补偿的逻辑。

最终,我选择了国内 X-Lab 开放实验室维护的、基于 GH Archive 数据的、存储在 ClickHouse 当中并提供相同查询接口的 GitHub Event 数据集。该数据集的数据模式定义是公开的,相比 ClickHouse.com 提供的 github_events 数据集,X-Lab 的数据集包含了实验室在开源数据指标工作中发现的有效提高数据可用性的字段,包括 repo_idactor_id 等等。此外,X-Lab 的数据集本身是其开源数据指标工作的数据基础,维护投入力度和问题响应速度更可靠。

在数据展示层面,我选择了 ClickHouse 去年官方支持的 Apache SuperSet 系统。你可以选择 follow 下面两个文档自建系统,或者直接使用 Preset 提供的 Free Tier 服务,其中已经预装了连接 ClickHouse 所需的组件。

配置好数据源并连接上展示系统之后,就可以开始探索社群活跃指标了。

评审人数人次

我们想要评估的活跃指标是参与 Issue 和 PR 处理与评审的状况,可以从最基本的指标开始做起:数数。

把活动范围限制在 Pulsar 主仓库当中,我们可以用如下 SQL 来取得一段时间内评审 PR 的人数:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT
toStartOfMonth(toDateTime(`created_at`)) AS timestamp,
COUNT(DISTINCT actor_login),
COUNT(DISTINCT actor_login, issue_number)
FROM `opensource`.`gh_events`
WHERE `created_at` >= '2022-01-01 00:00:00'
AND `created_at` < '2023-01-01 00:00:00'
AND `repo_name` = 'apache/pulsar'
AND `type` IN ('PullRequestReviewEvent',
'PullRequestReviewCommentEvent')
GROUP BY timestamp
ORDER BY timestamp

其中第一个 COUNT DISTINCT 结果表示的是在给定时间段内有多少个不同的 Reviewers 参与,而第二个 COUNT DISTINCT 的结果则是计算人次,即只把同一个人在同一个 PR 当中的 Review 动作合并,而不是把同一个人所有的 Review 活动都做合并。

在 SuperSet 的 UI 上做一些配置,我们可以把上面的 SQL 对应到下面的折线图:

Pulsar Reviews Count in 2022

可以看到,Pulsar 社群在去年一年的 Reviewer 数量维持在 60~70 人之间,而在 10 月之后 Review 人次计数有所下降。

对这个数字的解释,人数上,Pulsar 社群的活跃 Reviewer 群体没有大的变化。人次上,在年中开始发布 2.11 之后,出现了一系列测试出来的回退,核心开发者专注于解决这些回退,而解决回退最好是尽量少的改动,因此 PR 的总体新增数量少了。同时,cheery-pick 和处理回退的 PR 相对直观,往往只需要一两名 reviewer 赞同就已经合并,而不会像新功能一样有大量的成员发表意见。

交叉对比 PR 创建数量的图,可以印证这个想法:

Pulsar Pull Request Opened Count

最后,我们以 DISTINCT Reviewr 的人数为指标,比较一下知名的消息系统项目的活跃情况:

Reviewers Comparation

可以看到,在这一维度下,Pulsar 的活跃度是最高的,Kafka 次之;RocketMQ 和 RedPanda 是下一个梯队;而 ActiveMQ 和 RabbitMQ 的活跃度就快见底了。当然,这个数据只计算和主仓库的数值,而没有把所有仓库群加总起来算,但是加总的结果应当也是大同小异的。另一方面,核心能力的绝大部分开发工作还是发生在主仓库上,计算主仓库的活跃度可以见微知著。

平均响应时间

除了最基本的计数工作,我们还可以计算跨越一定时间的指标。例如,在工程效能乃至服务保障中都常包括的平均响应时间。结合我们这里想要评估的 Issue 和 PR 处理与评审的状况,我们可以分别计算 Issue 和 PR 从创建到首次被响应的时间,以及从创建到关闭的时间。

数据口径和挑战

不过,不同于简单计数指标,在事件粒度的数据集上做具有时间跨度的指标必定涉及数据之间的关联。如果不是类似 Apache Pulsar 活跃参与者每月看板这样,在取出数字后再经过程序处理,而是类似上面直接使用 SuperSet 作为一站式数据分析展示工具,那么关联数据的需求映射到 SQL 当中就是 JOIN 的语义,这会导致查询复杂度陡然上升一个量级,写出正确或者说合理的查询面临诸多挑战。

第一个是数据关联本身的复杂性。

要想知道具体一个 Issue 从创建到首次响应,再到被处理完毕一共花了多久,最准确的方法是通过 GitHub API 直接查询 Issue 的元数据,首先看它是否被关闭,如果被关闭,是何时被关闭。进一步地,列举 Issue 上的 comment 并取得最先提出的那个的时间,与 Issue 创建时间求差。

然而,这类数据是变化的,即使每天定时任务同步,也只能给特定的一个查询使用,从数据治理角度来说成本太高,最好是就写一个程序加上缓存硬算。这不仅脱离了 SuperSet 提供的框架,本身也会被 GitHub API 的 rate limit 限制。

从 GH Archive 数据集的事件信息出发,要想做这样的计算,就得首先取得 Issue 编号,再和其他 Issue 系列事件关联,适当排序后才能得到正确的结果。为了减少关联的复杂性,往往分析存储的每行会冗余信息。ClickHouse.com 和 X-Lab 的数据集都是一张大宽表,而 X-Lab 的数据更是每行都有 issue_created_atissue_closed_at 这样的预聚合信息。

第二个是数据关联对数据质量的高要求。

SQL 查询要想得到正确的结果,能不能正确关联上多个事件是非常重要的。而实际数据集至少会面临以下问题:

  1. 数据丢失。GitHub 本身的服务就是不稳定的,API 上能查到服务恢复后的状态,但是事件信息说丢了就是丢了。很多基于 GH Archive 数据集计算的结果在涉及 2021 年 10 月的数据时就容易出错,正是因为 GitHub 那一个月丢了近三天的事件数据。
  2. 异构社群。例如,有些社群会用机器人来做首次回复,计算社群活跃度计入和不计入机器人的行为显然是不一样的。而不同的社群有不同的自动化工具和流程,进行数据关联时需要处理这些细节问题。
  3. 不单调的数据。计算 Issue 创建到首次响应的指标相对还是简单的,因为第一次响应是个确定的指标,一旦得出结果就不再变化。但是从创建到关闭并不是,因为还有 Reopen 的情况。如果要把 Reopen 的情况考虑进去,数据口径和计算方法就需要重新评估。

第三个是关联时间的指标的口径差异。

OSSInsightOpenDigger 对上述两个时间数据的算法是以结束时间为基准的,即它们所说的 2022 年 12 月 Pulsar 社群 Issue 创建到关闭的平均时间,指的是在 2022 年 12 月被关闭的 Issue 从创建到关闭的平均时间。这样说你可能还没感觉出问题,我举个实例:我在 2022 年 11~12 月关闭了一大批年代久远的 Issue 和 PR 的行为导致 OSSInsight 上的指标在这两个月突然变差:

OSSInsight Pull Request Time to Merge

OSSInsight Issue Time to Respond

但是,实际上这与这两个月新创建的 Issue 和 PR 的处理实效关联不大。

同时,可以注意到 OSSInsight 只给出了 Issue 首次响应,以及 PR Merge 的时效。PR 关闭后可以重新打开,但是合并后则不再能转变状态。这一点,加上从结束时间倒算时间跨度,都是为了好算而做的选择。

下面,我会说明我所采用的指标口径、结果及社群之间的对比。

指标结果与对比

我所计算的时效指标,时间分区的逻辑是以开始时间作为基准的,即 2022 年 12 月 Pulsar 社群 Issue 创建到首次回应的平均时间,指的是在 2022 年 12 月期间新建的 Issue 被首次回应的平均时间。这个首次响应可能发生在 2022 年 12 月以后的任何一个时间。

不过,马上你就会发现两个问题:第一个是如果 Issue 迄今为止还没被响应,那这个时间就是未定义的;第二个是按照这种算法,计算出来的时效一定不会超过从创建到当前的时间,换言之,这个月的时效一定不会超过一个月。

为了辅助以开始时间作基准的指标逻辑,这里需要额外计算 Issue 和 PR 被处理的比例。我先展示最终计算得到的图表:

Pulsar 社群 Issue 和 PR 响应时效

右边一列就是区分所有在给定时间段内创建的 Issue 或 PR 的响应情况占比。可以看到,如同上面说明的,Issue 和 PR 创建的时间越近,其得到处理的比例就越小。而处理时效仅考虑被处理的情况,近期的数据总是相对较好。

实际在指导社群开展工作的时候,可以使用类似:在未处理 Issue/PR 比例不超过 N% 的前提下,平均响应时间不超过 M 小时。

不过,由于后来响应会导致原先的平均值被离群值拉高,所以在执行时可以采用分位数的维度来减少离群值的影响,同时仅在每月一号当天查看上个月的情况,而不是一直监控。

具体的计算 SQL 语句,以 PR 的回复时效为例,写作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
SELECT issue_number,
groupArray(b.actor_login)[1] AS author,
groupArray(actor_login)[1] AS reviewer,
dateDiff('hour', opened_at, commented_at) AS time_to_comment,
groupArray(b.created_at)[1] AS opened_at,
groupArray(created_at)[1] AS commented_at,
groupArray(type)[1] AS type,
groupArray(action)[1] AS action_type
FROM
(SELECT actor_login,
created_at,
type,
action,
issue_number
FROM `opensource`.`gh_events`
WHERE repo_name = 'apache/pulsar'
AND type IN ('PullRequestEvent',
'PullRequestReviewCommentEvent',
'PullRequestReviewEvent',
'IssueCommentEvent')
AND actor_login NOT IN ('github-actions[bot]',
'codecov-commenter')
ORDER BY created_at) a
JOIN
(SELECT actor_login,
created_at,
issue_number
FROM `opensource`.`gh_events`
WHERE repo_name = 'apache/pulsar'
AND type = 'PullRequestEvent'
AND action = 'opened') b ON a.issue_number = b.issue_number
WHERE a.actor_login != b.actor_login
GROUP BY issue_number
ORDER BY issue_number

由于 X-Lab 的数据做了一些预聚合,计算 PR 合并时效可以不用 JOIN 语句:

1
2
3
4
5
6
7
8
9
10
11
SELECT issue_number,
MIN(issue_created_at) AS opened_at,
MAX(pull_merged_at) AS merged_at,
dateDiff('hour', opened_at, merged_at) AS time_to_merge
FROM `opensource`.`gh_events`
WHERE repo_name = 'apache/pulsar'
AND type IN ('PullRequestEvent',
'PullRequestReviewCommentEvent',
'PullRequestReviewEvent')
GROUP BY issue_number
HAVING notEmpty(groupArray(pull_merged_at))

计算比例的 SQL 查询结构相同,但是在最外层 SELECT 仅保留 issue_numberopened_at 两列,并用 CASE WHEN 来区分类别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Respond --
(CASE
WHEN COUNT(DISTINCT actor_login) > 1 THEN 'respond'
WHEN notEmpty(groupArray(pull_merged_at)) THEN 'selfmerge'
WHEN notEmpty(groupArray(issue_closed_at)) THEN 'selfclose'
ELSE 'norespond'
END) AS status

-- Merge --
(CASE
WHEN notEmpty(groupArray(pull_merged_at)) THEN 'merged'
WHEN notEmpty(groupArray(issue_closed_at)) THEN 'closed'
ELSE 'open'
END) AS status

Issue 的情况大同小异,需要注意的是也需要从 IssuesEvent + opened 的查询里获取 issue_number 的值,而不能直接用 gh_events 里的 issue_number 字段,因为后者还包括 PR 的情况。

最后是同类社群之间做一个指标的比较。由于 Kafka 的 Issue 用 JIRA 来管理,同时不同体量的社群在时效上会有较大的区别,例如线下集中开发的为主的团体几乎总是能立即响应团队内部的需要,因此这里只对 PR 的响应和合并时效,针对 Apache Pulsar 社群和 Apache Kafka 社群做一个比较。

PR 响应时效的比较结果如下:

Pulsar vs. Kafka PR 响应时效

PR 合并时效的比较结果如下:

Pulsar vs. Kafka PR 合并时效

可以看到,在去年一年的时间里,Pulsar 的社群活跃度在这个维度上全面超过了 Kafka 社群。

小结

应该说,Pulsar 社群的活跃,一方面得益于其丰富的功能集带来的海量开发需求,另一方面也是社群多样化的成员组成带来的活力促进的。不同于 Kafka 上游主要依靠 Confluent 公司的雇员开发,一旦公司技术转向就会活力骤减,Pulsar 社群从提供商、集成商到应用企业,再到喜欢完整的 MQ 语义的个人开发者,不同人的需求在这里交汇和实现,与参与者同等数量的 Reviewer 参与评审,为软件长青提供了坚实的社群保障。

应该说,Pulsar 目前的开发者社群活跃度,还能够接纳不少寻找分布式消息系统的架构师,以及想要学习消息系统概念与实现的开发者。我在过去的半年里,逐步撰写了 Pulsar 社群的开发者指南,以期能够帮助到有意愿的开发者快速搭建一个本地开发环境,并参与到典型的开发活动当中来。此外,我还会在接下来的一两个月里,梳理出 Pulsar 社群生态的全貌,从而展示 Pulsar 生态的价值与其目前需要帮助的部分。按照《共同创造价值》的思路,这样就可以回答“我能为社群做什么”以及“应该怎么做到”的两个问题。

ClickHouse 社群指标模型

数字化时代下绝大多数工作都有关键绩效指标(KPI)指导,任何组织都期望找到一个合理的指标来校准战略方向,衡量工作成果。

然而,并非所有工作都能有 KPI 准确地衡量产出。著名软件工程师,同时也是《重构》《分析模式》等书籍的作者 Martin Fowler 曾经写过一篇博客论证软件工程师的生产力是无法衡量的

开源社群成员的重要组成部分,也是一切价值的核心飞轮就是这样一群软件工程师。这是否意味着开源社群的工作也是无法衡量的吗?

我认为,目前探索所及的一切开源社群指标都是人的活动产生的,并且这里的人都是匿名的网络用户。这样的背景下,如果有人背负着必须提高某个指标的任务,在不论对社群协作造成的次生影响的前提下,几乎所有指标都是可以做出来的。因此,只是考察社群指标的结果数字,甚至为了掩饰自己不愿意理解社群运作的深层逻辑,而要求组织的社群协调员以某种归一化的分数来汇报社群指标,都不能达到撬动社群杠杆完成组织生产力增效的目的。

但是,没有北极星指标引领社群工作方向,难免会导致社群成员对自己所在的社群正处于一种什么状态缺乏清晰的认识。不是将指标作为目标结果,而是以定性和定量的指标来辅助校准社群发展的方向和衡量社群工作的效率,这样的工作是有价值的。

Apache 成熟度模型就是一个定性模型,从代码、版权、发布、质量、社群、共识和独立性七个方面衡量一个软件项目是否符合 Apache 开源之道。绝大部分从 Apache 孵化器毕业的项目在毕业前都会准备一份对应成熟度模型的报告,例如 Apache Pulsar 在毕业之际就专门撰写了一份社群成熟度报告来回应孵化器导师的质询。

近年来,越来越多的企业和组织认识到社群运营的重要性。除了定性的指标以外,这些企业和组织投入的人力和资金也使得探索定量指标得到了更多的支持。这其中值得关注的两个,就是 orbit.love轨道模型和 ClickHouse 社群基于 GitHub Events 全域公开数据进行的社群分析。

相比之下,orbit.love 的轨道模型不只是针对开源社群,而是针对普遍的社群运营行为及其结果的建模,定义更加严谨,理论更加丰富。ClickHouse 社群的指标模型是从使用自家软件分析社群活动出发,目前还停留在比较简单的统计分析的阶段上。

本文以 ClickHouse 的社群分析报告为基础,看看从 GitHub Events 全域数据能够进行哪些数字化社群指标分析。

准备工作

本文的结果均基于 ClickHouse 提供的 GitHub Events 公开数据集查询产生,任何读者都应该可以复现相同的查询。由于数据集每天都会更新,因此不同时间查询的结果可能会有所出入。

要想查询这个公开数据集,最简单的方式是下载 ClickHouse 的命令行工具:

1
curl https://clickhouse.com/ | sh

如果一切顺利,应该可以看到 clickhouse 可执行文件下载成功,通过下面的命令连接到公开数据集:

1
./clickhouse client --secure --host play.clickhouse.com --user explorer

如果上述操作不成功,可以阅读官方的开始文档或者到项目 GitHub Discussions 的提问区提问。

成功连接到数据集后,可以查询全域被 star 过次数最高的仓库来作为本次旅途的 Hello world 步骤:

1
2
3
4
5
6
SELECT repo_name, count() AS stars FROM github_events WHERE event_type = 'WatchEvent' GROUP BY repo_name ORDER BY stars DESC LIMIT 1;
/*
┌─repo_name──────┬──stars─┐
│ 996icu/996.ICU │ 359029 │
└────────────────┴────────┘
*/

关联社群

探索关注到特定项目的参与者也会关注哪些其他项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
SELECT
repo_name,
count() AS stars
FROM github_events
WHERE (event_type = 'WatchEvent') AND (actor_login IN
(
SELECT actor_login
FROM github_events
WHERE (event_type = 'WatchEvent') AND (startsWith(repo_name, 'apache/pulsar'))
)) AND (NOT startsWith(repo_name, 'apache/pulsar'))
GROUP BY repo_name
ORDER BY stars DESC
LIMIT 5;
/*
┌─repo_name────────────────────────┬─stars─┐
│ ant-design/ant-design │ 3810 │
│ kubernetes/kubernetes │ 3540 │
│ donnemartin/system-design-primer │ 3416 │
│ pingcap/tidb │ 3226 │
│ golang/go │ 3225 │
└──────────────────────────────────┴───────┘
*/

SELECT
repo_name,
count() AS stars
FROM github_events
WHERE (event_type = 'WatchEvent') AND (actor_login IN
(
SELECT actor_login
FROM github_events
WHERE (event_type = 'WatchEvent') AND (startsWith(repo_name, 'apache/flink'))
)) AND (NOT startsWith(repo_name, 'apache/flink'))
GROUP BY repo_name
ORDER BY stars DESC
LIMIT 5;
/*
┌─repo_name─────────────┬─stars─┐
│ apache/spark │ 6722 │
│ tensorflow/tensorflow │ 6515 │
│ 996icu/996.ICU │ 5680 │
│ ant-design/ant-design │ 5551 │
│ kubernetes/kubernetes │ 5461 │
└───────────────────────┴───────┘
*/

可以看到,这个指标下被筛选出来的项目往往是热门项目,而不是与自己社群相关的其他项目。只有类似 Flink 和 Spark 这样同时都是热门项目的情况,才有可能把关联项目筛选出来。这也侧面提示了前文提到的,指标只能提供侧面辅助作用,最终需要熟悉社群运作和实际运行情况的人来解释。

为了提高关联度,我们筛选项目关注人员里,同时关注我们项目的人占比最高的那些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
SELECT
repo_name,
uniq(actor_login) AS total_stars,
uniqIf(actor_login, actor_login IN
(
SELECT actor_login
FROM github_events
WHERE (event_type = 'WatchEvent') AND (startsWith(repo_name, 'apache/pulsar'))
)) AS our_stars,
round(our_stars / total_stars, 2) AS ratio
FROM github_events
WHERE (event_type = 'WatchEvent') AND (NOT startsWith(repo_name, 'apache/pulsar'))
GROUP BY repo_name
HAVING total_stars >= 100
ORDER BY ratio DESC
LIMIT 5;
/*
┌─repo_name──────────────────┬─total_stars─┬─our_stars─┬─ratio─┐
│ streamnative/pulsarctl │ 105 │ 81 │ 0.77 │
│ kuangye098/Pulsar-analysis │ 107 │ 75 │ 0.7 │
│ bbonnin/pulsar-express │ 105 │ 71 │ 0.68 │
│ streamnative/pulsar-flink │ 260 │ 165 │ 0.63 │
│ streamnative/kop │ 332 │ 196 │ 0.59 │
└────────────────────────────┴─────────────┴───────────┴───────┘
*/

SELECT
repo_name,
uniq(actor_login) AS total_stars,
uniqIf(actor_login, actor_login IN
(
SELECT actor_login
FROM github_events
WHERE (event_type = 'WatchEvent') AND (startsWith(repo_name, 'apache/flink'))
)) AS our_stars,
round(our_stars / total_stars, 2) AS ratio
FROM github_events
WHERE (event_type = 'WatchEvent') AND (NOT startsWith(repo_name, 'apache/flink'))
GROUP BY repo_name
HAVING total_stars >= 100
ORDER BY ratio DESC
LIMIT 5;
/*
┌─repo_name───────────────────────────┬─total_stars─┬─our_stars─┬─ratio─┐
│ ververica/flink-jdbc-driver │ 122 │ 89 │ 0.73 │
│ docker-flink/docker-flink │ 154 │ 112 │ 0.73 │
│ flink-extended/flink-remote-shuffle │ 163 │ 119 │ 0.73 │
│ nexmark/nexmark │ 180 │ 124 │ 0.69 │
│ ververica/stateful-functions │ 268 │ 182 │ 0.68 │
└─────────────────────────────────────┴─────────────┴───────────┴───────┘
*/

可以看到,项目的相关性大幅增强了。进一步地,我们可以通过增加项目集合的大小或筛选已知项目来逐步探索生态项目。

除了 star 这样动动手指就能做到的动作,如果一个参与者提出实际提出一个问题、提交一个补丁或者参与讨论,这样的行为所需要的动机更加强烈。我们也可以试着从 issue 和 PR 的角度来观察项目之间的相关性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
SELECT
repo_name,
count() AS prs,
uniq(actor_login) AS authors
FROM github_events
WHERE (event_type = 'PullRequestEvent') AND (action = 'opened') AND (actor_login IN
(
SELECT actor_login
FROM github_events
WHERE (event_type = 'PullRequestEvent') AND (action = 'opened') AND startsWith(repo_name, 'apache/pulsar')
)) AND (NOT startsWith(repo_name, 'apache/pulsar'))
GROUP BY repo_name
ORDER BY authors DESC
LIMIT 5;
/*
┌─repo_name──────────────┬──prs─┬─authors─┐
│ apache/bookkeeper │ 1761 │ 96 │
│ apache/flink │ 726 │ 46 │
│ streamnative/kop │ 529 │ 34 │
│ apache/spark │ 1033 │ 28 │
│ streamnative/pulsarctl │ 227 │ 26 │
└────────────────────────┴──────┴─────────┘
*/

SELECT
repo_name,
count() AS issues,
uniq(actor_login) AS authors
FROM github_events
WHERE (event_type = 'IssuesEvent') AND (action = 'opened') AND (actor_login IN
(
SELECT actor_login
FROM github_events
WHERE (event_type = 'IssuesEvent') AND (action = 'opened') AND startsWith(repo_name, 'apache/pulsar')
)) AND (NOT startsWith(repo_name, 'apache/pulsar'))
GROUP BY repo_name
ORDER BY authors DESC
LIMIT 5;
/*
┌─repo_name─────────────────┬─issues─┬─authors─┐
│ apache/bookkeeper │ 815 │ 105 │
│ streamnative/kop │ 287 │ 45 │
│ streamnative/pulsar-flink │ 163 │ 42 │
│ kubernetes/kubernetes │ 69 │ 41 │
│ golang/go │ 139 │ 40 │
└───────────────────────────┴────────┴─────────┘
*/

可以看到,Pulsar 与部署集群的直接依赖 BookKeeper 项目,以及生态伙伴 Flink 项目和 Spark 项目是紧密联系的。StreamNative 围绕 Pulsar 打造了一系列的开源生态项目。

成员活动

接下来我们把目光放到单个项目群之内,看看社群当中的最活跃的 Code Reviewer 是谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SELECT
actor_login,
count(),
uniq(repo_name) AS repos,
uniq(repo_name, number) AS prs
FROM github_events
WHERE (event_type = 'PullRequestReviewCommentEvent') AND (action = 'created') AND startsWith(repo_name, 'apache/pulsar')
GROUP BY actor_login
ORDER BY count() DESC
LIMIT 5;
/*
┌─actor_login───┬─count()─┬─repos─┬─prs─┐
│ sijie │ 1828 │ 8 │ 531 │
│ codelipenghui │ 1599 │ 2 │ 648 │
│ eolivelli │ 1482 │ 5 │ 592 │
│ Anonymitaet │ 1306 │ 8 │ 375 │
│ merlimat │ 1272 │ 3 │ 579 │
└───────────────┴─────────┴───────┴─────┘
*/

这和绝大部分 Pulsar 参与者的观感是一致的。创始成员 @sijie 和 @merlimat 参与度非常高,目前分别就职于 StreamNative 和 Datastax 的研发领导者 @codelipenghui 和 @eolivelli 位列前三,Pulsar 文档的维护者 @Anonymitaet 也强于积极沟通。

我们可以通过扩大 LIMIT 数量和排除已知的活跃成员,或者将事件按照时间筛选和分区,来发现更多潜在的活跃参与者。

我们还可以聚合成员活动观察社群发展趋势,例如 Pulsar 项目群的 PR 数目在过去两年间的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SELECT
toStartOfMonth(created_at) AS date,
count() AS total_prs,
uniq(actor_login) AS unique_actors,
bar(unique_actors, 0, 200, 100)
FROM github_events
WHERE startsWith(repo_name, 'apache/pulsar') AND (event_type = 'PullRequestEvent') AND (action = 'opened') AND (created_at > '2020-01-01 00:00:00')
GROUP BY date
ORDER BY date ASC;
/*
┌───────date─┬─total_prs─┬─unique_actors─┬─bar(uniq(actor_login), 0, 200, 100)───────────┐
│ 2020-01-01 │ 149 │ 56 │ ████████████████████████████ │
│ 2020-02-01 │ 161 │ 62 │ ███████████████████████████████ │
│ 2020-03-01 │ 142 │ 47 │ ███████████████████████▌ │
│ 2020-04-01 │ 168 │ 58 │ ████████████████████████████▊ │
│ 2020-05-01 │ 191 │ 63 │ ███████████████████████████████▌ │
│ 2020-06-01 │ 208 │ 54 │ ███████████████████████████ │
│ 2020-07-01 │ 229 │ 74 │ █████████████████████████████████████ │
│ 2020-08-01 │ 168 │ 55 │ ███████████████████████████▌ │
│ 2020-09-01 │ 190 │ 75 │ █████████████████████████████████████▌ │
│ 2020-10-01 │ 166 │ 60 │ ██████████████████████████████ │
│ 2020-11-01 │ 251 │ 60 │ ██████████████████████████████ │
│ 2020-12-01 │ 250 │ 62 │ ███████████████████████████████ │
│ 2021-01-01 │ 200 │ 63 │ ███████████████████████████████▌ │
│ 2021-02-01 │ 249 │ 71 │ ███████████████████████████████████▌ │
│ 2021-03-01 │ 245 │ 58 │ ████████████████████████████▊ │
│ 2021-04-01 │ 251 │ 60 │ ██████████████████████████████ │
│ 2021-05-01 │ 221 │ 68 │ ██████████████████████████████████ │
│ 2021-06-01 │ 325 │ 89 │ ████████████████████████████████████████████▌ │
│ 2021-07-01 │ 248 │ 83 │ █████████████████████████████████████████▌ │
│ 2021-08-01 │ 193 │ 70 │ ███████████████████████████████████ │
│ 2021-09-01 │ 289 │ 70 │ ███████████████████████████████████ │
│ 2021-10-01 │ 33 │ 19 │ █████████▌ │
│ 2021-11-01 │ 383 │ 77 │ ██████████████████████████████████████▌ │
│ 2021-12-01 │ 402 │ 90 │ █████████████████████████████████████████████ │
│ 2022-01-01 │ 377 │ 77 │ ██████████████████████████████████████▌ │
│ 2022-02-01 │ 289 │ 85 │ ██████████████████████████████████████████▌ │
│ 2022-03-01 │ 352 │ 78 │ ███████████████████████████████████████ │
│ 2022-04-01 │ 314 │ 77 │ ██████████████████████████████████████▌ │
│ 2022-05-01 │ 345 │ 86 │ ███████████████████████████████████████████ │
│ 2022-06-01 │ 325 │ 88 │ ████████████████████████████████████████████ │
│ 2022-07-01 │ 306 │ 76 │ ██████████████████████████████████████ │
└────────────┴───────────┴───────────────┴───────────────────────────────────────────────┘
*/

除了 2021 年 11 月的数据部分丢失以外,可以看到 Pulsar 社群每月独立贡献者数量从 50 人左右成长到 80 人左右。如果我们观察一个异军突起的社群,这种变化会更明显。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
SELECT
toStartOfMonth(created_at) AS date,
count() AS total_prs,
uniq(actor_login) AS unique_actors,
bar(unique_actors, 0, 200, 100)
FROM github_events
WHERE startsWith(repo_name, 'bytebase') AND (event_type = 'PullRequestEvent') AND (action = 'opened')
GROUP BY date
ORDER BY date ASC;
/*
┌───────date─┬─total_prs─┬─unique_actors─┬─bar(uniq(actor_login), 0, 200, 100)─┐
│ 2021-03-01 │ 2 │ 1 │ ▌ │
│ 2021-07-01 │ 2 │ 2 │ █ │
│ 2021-08-01 │ 2 │ 2 │ █ │
│ 2021-09-01 │ 2 │ 2 │ █ │
│ 2021-11-01 │ 37 │ 7 │ ███▌ │
│ 2021-12-01 │ 226 │ 10 │ █████ │
│ 2022-01-01 │ 216 │ 14 │ ███████ │
│ 2022-02-01 │ 154 │ 16 │ ████████ │
│ 2022-03-01 │ 320 │ 19 │ █████████▌ │
│ 2022-04-01 │ 288 │ 22 │ ███████████ │
│ 2022-05-01 │ 361 │ 19 │ █████████▌ │
│ 2022-06-01 │ 316 │ 19 │ █████████▌ │
│ 2022-07-01 │ 345 │ 21 │ ██████████▌ │
└────────────┴───────────┴───────────────┴─────────────────────────────────────┘
*/

SELECT
toStartOfMonth(created_at) AS date,
count() AS total_prs,
uniq(actor_login) AS unique_actors,
bar(unique_actors, 0, 200, 100)
FROM github_events
WHERE startsWith(repo_name, 'datafuselabs') AND (event_type = 'PullRequestEvent') AND (action = 'opened')
GROUP BY date
ORDER BY date ASC;
/*
┌───────date─┬─total_prs─┬─unique_actors─┬─bar(uniq(actor_login), 0, 200, 100)─┐
│ 2021-02-01 │ 2 │ 1 │ ▌ │
│ 2021-03-01 │ 91 │ 8 │ ████ │
│ 2021-04-01 │ 163 │ 12 │ ██████ │
│ 2021-05-01 │ 135 │ 13 │ ██████▌ │
│ 2021-06-01 │ 155 │ 16 │ ████████ │
│ 2021-07-01 │ 188 │ 17 │ ████████▌ │
│ 2021-08-01 │ 239 │ 28 │ ██████████████ │
│ 2021-09-01 │ 222 │ 25 │ ████████████▌ │
│ 2021-10-01 │ 42 │ 13 │ ██████▌ │
│ 2021-11-01 │ 361 │ 36 │ ██████████████████ │
│ 2021-12-01 │ 326 │ 43 │ █████████████████████▌ │
│ 2022-01-01 │ 274 │ 34 │ █████████████████ │
│ 2022-02-01 │ 207 │ 36 │ ██████████████████ │
│ 2022-03-01 │ 292 │ 34 │ █████████████████ │
│ 2022-04-01 │ 383 │ 37 │ ██████████████████▌ │
│ 2022-05-01 │ 404 │ 47 │ ███████████████████████▌ │
│ 2022-06-01 │ 444 │ 45 │ ██████████████████████▌ │
│ 2022-07-01 │ 300 │ 40 │ ████████████████████ │
└────────────┴───────────┴───────────────┴─────────────────────────────────────┘
*/

随着核心团队获得资本投资,持续招募全职人员并在市场上释放协同信号,Bytebase 和 DatafuseLabs 的开源项目群都实现了从 0 到 1 乃至小有规模的增长。

总结

ClickHouse 社群指标模型的建设应该开始于 ClickHouse 10616 号议案

对于相当部分开源数据处理项目来说,分析自己的开源社群的指标,是一种自然的展示项目价值,并且自己成为自己所制造的软件的用户的好途径。例如,TiDB 在完成 HTAP 能力生产可用建设以后,就发起了 OSSInsight 项目。虽然在指标思考上不如 ClickHouse 做得这么丰富,但是前端展示水平上要惊艳许多。

另外,ClickHouse 社群的工作也得到了其他致力于开源社群指标衡量的工作小组的关注和引用。例如,X-Lab 开放实验室的赵生宇博士就在其分析工具 Open Digger 当中展示了 ClickHouse GitHub Explorer 的样例

其实,本文当中没有涉及 ClickHouse 社群分析报告里大量全域分析的内容。例如,GitHub 上名称最长的仓库是哪个,star 增长最快的是哪个,PR 最多的是哪个。这些查询虽然有趣,但是对于指导具体社群运营的前进方向提供的帮助则比较有限,只是简单的统计工作。

不过,一个公开可得的数据集,如果有数据开发工程师投入时间研究写出高水平的查询,还是未来可期。一份公开可用而且好用的数据集本身就是对开源指标衡量的重要贡献,GHArchive 原本的数据可远远称不上好用。

对于数据建模和质量方面,ClickHouse 的数据两个比较明显的问题。一个是可能数据会缺失,例如上面我们看到的 2021 年 11 月 PR 创建数据丢失。这是 GHArchive 数据集本身的问题,对这些数据的补偿修复工作,CNCF 的 Devstats 项目有一些经验。另一个是 ClickHouse 只记录用户和仓库名而不是唯一标识的 ID 字段,这就使得重命名过的用户、组织和仓库的对应关系需要先验知识补充。当然,即使记录了唯一标识,也有不同账户实际上是同一个自然人的情况,以及项目群聚类时需要合并计算的情况,这都是一般的 ID Mapping 挑战。

此外,ClickHouse 的数据集是历史全量数据,因此适合用于分析历史变化趋势,不同时间段的人群圈选。对于发现目前社群状态下立刻需要解决的问题,例如目前积压 Requested Review 最多的维护者是谁,以提醒或帮助他处理 backlog 这样的需求,直接调用 GitHub REST 或 GraphQL API 查询当前信息会更加合适。

我会在 Neptune 项目里逐渐把这两年来发现的指标以网页的形式总结展示出来,希望在目前简单统计 star 数和 contributor 数量的基础上,为指标指导社群工作分享一些心得。不过由于我对前端知识的欠缺、指标开发固有的时间开销,以及本职工作的时间占用,可能进度不会很快。欢迎有前端展示经验的同学评审代码,也欢迎社群运营同学交流经验和提出需求。

赵生宇博士在今年初的时候写过一篇文章《开放协作的世界里,每一份贡献都值得回报》,议论了开源度量为开源生态发展可能带来的价值。结合 ClickHouse 社群自己探索社群指标模型,OSSInsight 项目的成立和发展,Open Digger 持续的迭代,我们有理由相信,度量指标助力社群运营,帮助更多开源社群定位自己目前的健康状况和潜在的发展改进方向将逐渐成为现实。

❌