前言
2024 年 10 月底,我正在和协会里的同学一起准备实习的事情,适时我已经通过了 AutoMQ 和深信服的二轮面试,不过就在同一个星期,小红书存储数据库平台的负责人(也就是我现在的 mentor)通过短信联系到我,然后我在一天的时间内通过三轮面试,拿到这个岗位的实习资格。
工作
小红书的工作强度还是比较高的,我目前的基本上是早10晚9大小周,偶尔几次晚9点半,当然对于实习生而已,不同组的工作量差异比较大,我旁边的实习生早10晚8,也有的同学早10晚6。平时要做什么一般是 mentor 安排的,也有一些是组内的群里面收到 bug 反馈,自己主动处理的。此外我们组还会有日会和周会同步进度,周会额外需要简单整理个人周报。
生产工具
生产工具的配置还是不错的
- Macbook Pro 14' M3 18G + 512G
- 27' 4K 显示器 * 2
- 罗技办公鼠标(我习惯用触摸板,没要)
- GitHub Copilot
- Navicat Premium
技术栈
后端:
- Python3.11
- Golang1.22 & 1.23
- MySQL 5 & MySQL 8
- ClickHouse (内部版)
- Kafka
- Airflow
前端:
- Vue3
- Webpack
运维:
- Docker
- K8s
工作内容
现在的服务基本上都是微服务架构,所以个人主要是负责模块或者包的开发和维护,我在组内主要负责 DMS (Data Management Service) 和 DAS (Database Autonomy Service) 的开发和维护。
Airflow 日志
Apache Airflow 是平台的核心之一,负责平台各类任务调度和运行,在这个模块中,我主要负责将 Airflow 的日志和平台打通,使得用户可以在现有平台查看自己有权限访问的任务日志,而不同额外获取权限访问 Airflow。
这是一个相对简单的需求,我所做的是在平台的后端调用 Airflow 的 API 获取日志,然后返回给前端,前端调用一个展示长文本的组件,并提供日志页面的入口;同时对于平台管理员,提供了一个跳转 Airflow 的入口。
这个需求是我入职后的第一个需求,主要是帮助我熟悉平台和 Airflow 引擎。
企微审批
由于平台有大量工单,这些工单往往需要经过多个负责人审批,不过由于平台仅能通过内网访问,当一个工单的负责人不在内网时,需要等待负责人回到内网后,才能进行审批,这无疑是一个卡点。因此这个需求的目标是,当工单需要审批时,平台服务号自动发送审批消息到负责人,负责人可以选择在企微上进行审批,或者跳转到平台进行审批,二者实现的效果是相同的。
在经过充分调研后,我确定了使用企微的按钮卡片消息 + 自建回调消费网关的方案:
由于企微的回调消息本质上是通过 webhook 发送的一个 HTTP 请求,也不能直接访问到内网,因此公司内部有一个中台服务,专门负责处理企微的回调,将回调结果解析后,放入 Kafka 中,然后我们的平台消费 Kafka 中对应的 topic 的消息,获取回调结果。但是这意味这这条链路丢失了环境(prod/beta),所以我还需要事先在回调的消息中增加一些标识字段,然后对回调结果进行解析,获取消息中定义的 Action
, Env
等字段,然后路由到对应的环境的服务中。
回调消息链路如下:
在这个需求中,并没有涉及到复杂的逻辑,其中 Kafak 广播模式的接入,我在以前也在 RabbitMQ 上做过类似的接入,并没有什么难点,所以在完成模块的设计后,大部分的时间都花在了对企微回调消息的兼容性上,以确保回调消息能够被平台消费服务正确解析。
为了保证不同平台的状态一致性,我们需要保证审批前后,平台和企微所展示的工单状态一致,因此我们需要在审批完成时,通过企微的 更新模板卡片消息 接口,更新工单状态。这个 API 需要使用到 response_code
字段,这个字段是企微在发送模板卡片消息时,返回的一个唯一标识,且只能调用一次,因此我们需要在发送模板卡片消息时,将这个字段保存下来,由于我们使用的是 Airflow 管理任务,这个字段也就保存在了任务的 context
中。
企微消息封装
平台需要通过企微服务号发送消息,但是之前为了新需求的快速迭代,企微的消息更多是使用 markdown 格式,这种消息在企微的客户端上显示效果并不好,因此在平台趋于稳定后,我封装了企微卡片消息,并将平台现有的大部分消息替换成卡片消息,以提升平台专业性和易用性。
值得一提的是按钮卡片消息,由于企微对于回调消息的字符集有严格限制,只能使用英文大小写、数字和部分符号,因此在封装的时候额外补充了文档。
告警订阅
针对业务侧对告警配置缺乏积极性的现状,结合存储服务告警规则高度同质化的特点,这个需求通过预置存储类型化告警模板和实施标准化订阅接口,实现告警策略的快速部署能力。该方案有效降低了配置复杂度,使业务方能够通过统一入口完成批量告警策略的快速订阅。
在老平台上,这个需求已经被实现,但由于老平台是 go1.11 开发的,而新平台是 Python3.11 开发的,且由于新平台对告警的订阅需求的精简,老平台上的一些设计在现在的眼光来看过于臃肿,所以我在迁移功能的同时,也进行了一些优化。比如对于编辑告警订阅,老平台是先查询当前订阅规则的所有内容,然后显示部分内容,用户编辑后,将所有内容提交,这个过程中,相当于用户可以编辑任意内容,而在新平台中,我需要将编辑内容限制在当前订阅规则的范围内,当用户编辑了其他内容时,不会生效。
这降低了用户编辑的门槛,同时减少了用户误操作的风险,而对于平台管理员,我还在前端提供了前往 '可观测平台' 的入口,那里可以对告警进行更加深度的配置。
Airflow 性能优化
如前文所述,Airflow 是平台的核心之一,但是它的性能并不理想,对于一个简单的任务,需要 20 秒以上才能完成,这对于一个平台来说,显然是不够的。因此我们需要优化 Airflow 的性能,使得当我们同时运行 100 个简单的任务时,能够在 5 分钟以内完成。
在参考了大量资料后,我通过更改 Airflow 的一些配置,在同样的数据量下,缩短了近30%的运行时间,但是这个提升并不理想,而且如果使用复杂任务,这个提升会进一步降低,由于时间有限,我并没有继续深入优化,在完善了调优的文档后,我就转向了其他需求。
MySQL 活动采集
MySQL性能监控的核心在于实时捕获数据库运行时关键指标,包括会话状态跟踪、慢SQL识别、锁竞争监控等维度数据。这些实时运行指标不仅直接反映数据库运行状态,更为后续的性能优化提供数据支撑。为此我们建立了分钟级的采集机制,通过流式数据处理架构将数据持久化至 ClickHouse 中,为 DBA 团队提供实时诊断依据,同时支撑数据分析团队构建索引优化模型。
在原有平台上,这个功能的实现是相对割裂的,比如慢查询的采集,一些集群是通过实例上 filebeat
采集,再存入 Kafka 中,然后由平台消费 Kafka 中的消息,最后存入一个公共的 Clickhouse 集群中,而一些是通过腾讯云 cdb 的慢查询日志 API 获取,再存入 Clickhouse 中;但是 session 连接的采集,则是通过平台自身不断向众多 MySQL 实例发送 show processlist
命令,然后存入平台的 MySQL 当中,这也严重拖累了平台的性能。
在这个需求中,我需要将 MySQL 的采集服务与 DMS 平台分离,单独放在 DAS 服务中,作为数据基座的一部分。
由于 MySQL 实例的数量非常庞大,且动态变化,因此对于 session 连接、锁等待等数据的采集,我设计了一个分布式采集服务,架构如下:
在这个架构中:
- K8s 集群中运行 4 个采集服务 Pod
- 每个 Pod 中有一个 Manager 进程和多个 Collector 进程
- Manager 进程定期轮询 MySQL Instance DB 获取需要采集的实例信息
- Manager 进程根据实例信息动态创建或销毁 Collector 进程
- Collector 进程负责实际的数据采集工作,并将结果写入 ClickHouse Buffer 层中
由于这是一个分布式的采集服务,还需要考虑负载均衡的问题,在参考了几种主流的负载均衡方案后,我选择了基于 K8s 的 pod id 的分片方案。由于 pod id 唯一且递增,我们可以将 pod id 作为分片键,将 MySQL 实例均匀分配到不同的采集服务 Pod 中,从而实现负载均衡。而且由于实例总数增长有限,所以暂时不用考虑实例扩建导致的问题。
ClickHouse 对于写入的频率存在一定限制,高频的写入会导致 ClickHouse 的性能下降,因此我增加了 buffer 层,不同 collector 采集到的数据会通过 Golang 的 channel 统一写入到 buffer 层中,然后由一个单独的进程基于简单的刷盘逻辑,将 buffer 中的数据批量写入到 ClickHouse 中。
为了方便拓展采集的内容,我使用了 Golang 的静态生成代码的能力,通过解析 Golang 的 struct tag,生成将 Golang struct 转换为 ClickHouse 写入所需的有序的 interface array 的接口函数,从而简化业务代码,降低代码耦合度。在整个采集服务中,开发者只需要关心 struct 的定义、数据采集的 SQL 和 ClickHouse 的表结构,而无需关心 ClickHouse 的写入逻辑以及数据类型的转换,大幅降低了横向拓展的难度和成本。
MySQL 空间数据采集
MySQL 空间数据采集是对于 MySQL 的表空间、索引、列、日志等空间数据的采集,这些空间数据反映了一个 MySQL 实例的存储空间的使用情况,对于业务侧的优化和排查问题有非常大的帮助。
在 MySQL 活动采集的基础上,我扩展了 MySQL 空间数据采集。与活动采集不同的是,空间数据采集中日志大小等空间数据需要使用 SSH 连接到 MySQL 实例或者调用 cdb 的 API,因此我对于不同的实例类型,封装了不同的包,以简化采集服务的开发。比如对于 SSH 连接的实例,我封装了 remote
包,里面包含了创建 SSH 连接和多种命令执行的函数;而对于 cdb 的实例,我封装了 cdb
包,里面维护了一个使用 channel 实现的 cdb 连接池,以限制请求的频率,并封装了获取 binlog 空间等信息的方法。
MySQL 实例元数据采集
MySQL 实例元数据采集的目标是实现一个可以动态配置采集任务的采集服务,这个需求主要是为了方便平台的开发同学快速配置采集任务,而无需手动修改采集服务的代码,再重新部署。比如在原有的平台上,MySQL 实例的磁盘容量等信息在实例创建后不会及时更新,需要 DBA 同学完成扩容操作后手动维护,而通过这个新功能,开发同学可以在平台上快速配置采集任务,然后由采集服务自动完成磁盘容量的采集,并更新到平台的实例元数据表中,并使用最新数据进行展示。
在这个需求的实现上,我使用了公司的分布式定时任务平台,将 Golang 的采集逻辑编译成一个可执行的二进制文件,在执行的时候动态获取采集任务的配置,然后由定时任务平台进行调度,以简化诸如手动触发、失败重试等操作。
由于元数据表是存储在 MySQL 实例上的,我使用的是 insert ... on duplicate key update
语句,以简化批量更新元数据表的操作。但是更新的数据量非常大,单次采集的更新条数在百万级,这也使得单次采集任务(采集+更新)耗时来到了 20 分钟以上,所以需要对采集任务进行优化。
我首先想到的办法是创建多个 MySQL 连接,然后通过多个连接并行更新,这个方法成功将采集任务的耗时降低到 15 分钟,而且这个方法存在一个致命的问题:MySQL 的 insert ... on duplicate key update
语句在执行时,会创建隔行锁 (Gap Locks),即便我确定更新的是不同的行、不同的索引,也可能导致锁死锁,需要重试,而重试的次数过多,同样会导致性能下降。我尝试将单次更新的行数限制在 500 行,以减少锁冲突的概率,但是这个方法的耗时仍然在 10 分钟以上,所以需要进一步优化。
经过进一步分析,我决定采用生产者-消费者模型进行并发优化。简单理解,我创建了两个独立的 Goroutine:数据采集协程作为生产者,将实时采集的元数据写入一个缓冲 Channel;数据更新协程作为消费者,使用 for range
循环持续从 Channel 中读取数据块,缓冲区大小为 1000 行。
在更新策略上,采用连接池管理 MySQL 连接,并实施批量更新机制。每个更新批次会:
- 累积 500 行数据后自动触发
- 实施指数退避重试机制处理死锁
该方案实现了采集与更新的流水线并行,在生产环境中:
- 整体耗时从 15 分钟降至 4 分 20 秒(较初始方案提升 4 倍)
- MySQL 连接数减少 50%(从 12 个连接减少至 6 个)
- 锁冲突概率下降 99%(通过缩短事务持有时间和行级锁粒度控制)
- 内存消耗稳定在 100MB 以内,避免了 OOM 的风险
其他
当然还有一些平台小修小补,尤其是前端的 bug,诸如:宽度错误导致组件换行、为了兼容 URL 获取参数导致无法自动刷新数据等等,这里就不一一列举了。
比较指的一提的是 ClickHouse 的使用,这是一个分布式列式数据库,对于大量条数的查询速度非常快,而且使用的存储空间也更加小。相比我之前使用其他的数据库,在这个库上,我使用 LIMIT ... BY
简化查询语句;并且使用到了物化视图聚合不同维度下 MySQL 的慢查询、锁等待、连接数等数据,以方便在平台中进行展示,并减少约 50% 的查询耗时。我之后应该还会再深入了解一下 ClickHouse 的一些能力和特性,如果有时间的话,可能会单独写一篇博客来探讨一下。
工作总结
这段时间的工作还是比较充实的,也学到了很多东西,比如 Golang 静态生成代码、ClickHouse 的使用、K8s 负载均衡、Airflow 的使用和调度、MySQL 的特性、filebeat 和 eBPF 的使用等等。年后回去还有一些新的需求,希望可以继续学习,无限进步。
生活
规律作息,健康饮食。
住房
我和我的室友在浦东合租了一套两室一厅的房子,房租是 5500 元/月,民用水电,免物业费,年后还有一个室友搬进来,房租平摊下来还能接受。
通勤
公司在黄浦,去程需要 1 小时左右,回程需要 1 小时 10 分钟左右,每天通勤时间在 2 小时左右,每天回到住处已经很晚了,早上的时间基本都花在了通勤上。
饮食
在10点前到公司有免费早餐,午餐晚餐我平时基本都在公司的餐饮平台上订餐,一般一周最后一天会和同事出去吃。
公司的茶水间有免费的咖啡、茶包。
每个月有 300 零食积分(相当于 100 RMB),可以用来在零食柜换零食(听说以前零食柜随便吃,不过去年开始零食柜需要积分了)。
娱乐
没有什么娱乐时间,晚上回去已经比较晚了,还要补学校的功课(也就期末几周)。
人际
在公司大家都比较友好,我的工作涉及多个中台的对接,需要和多个不同部门的同学联系,就目前来看,大家还是很乐于给予帮助的。不过平时大家都很忙,很少有工作之外的交流。
实习收获与展望
通过近三个月的全栈开发实习,我获得了以下成长与认知:
技术层面
- 深入实践了微服务架构下的模块化开发模式,并了解了分布式系统的设计与实现
- 掌握了企业级消息队列(Kafka)与工作流引擎(Airflow)的集成应用
工作认知
- 互联网企业的敏捷开发节奏要求快速理解需求与交付价值
- 规范的Code Review机制有效提升代码质量
- 跨团队协作中清晰的技术方案文档至关重要
未来方向
- 深入理解云原生技术栈在数据库领域的应用
- 学习分布式系统设计模式
- 提升技术方案文档的撰写能力
这段经历让我认识到,优秀的工程师不仅要写出能跑的代码,更要构建可维护、可观测、可扩展的系统。期待在后续实习中继续突破技术舒适区,无限进步。