Posts
2023,重新起航
许久没有写博客,也从来没有写过公众号,那就从这篇年终总结开始吧。
2022 年对我个人来说是一个重大转折的一年。经过许久的犹豫、挣扎和思考,最终下定决心从大厂出来创业,我这艘不甘于稳定、日复一日生活的小帆船,又开始重新起航,不过这次,我的身边有一群小伙伴。
创业的动机跟我过去 4 年的工作有关,在解决超大规模互联网技术平台的可用性保障过程中,我们自研了一整套时序数据处理系统,通过采集丰富的可观测数据,构建可观测实体的模型以及它们之间的关联关系,结合海量数据挖掘技术、专家经验和智能算法(或者称之为 AIOps),我们构建了一个可观测的实时数据集市,来解决软件服务的异常巡检、可用性监控、预警及根因定位等问题,达到了非常好的效果,并且这样的数据集市在构建完成之后,在更多业务场景中也找到用武之地,例如容器资源的弹性智能调度、全站的资源和性能诊断优化等。
这个过程中,我抽象出三个底盘的理念:
数据底盘:也就是上文提到的实时数据集市,解决数据的采集、清洗、建模(实体及关系)、治理等。将所有层次(物理层、IaaS、网络、中间件、数据库、应用、业务、大数据生态)等数据汇总到一个时序数据湖里,打破数据孤岛,让用户可以找到数据,数据可以找到应用。
算法底盘:在大量的数据面前,依靠人工挖掘分析是不够的,我们需要提供算法的能力来帮助用户挖掘和分析数据。比如一个配置预警阈值的场景,当监控指标超过 10 万的时候,不可能靠人去一个一个去配置了,你需要智能的时序异常检测及预测算法来帮助你。同时,也需要建设一个算法平台来帮助算法研发、测试和运行。
代码底盘:仅有算法也是不够的,深度学习模型需要大量的高质量的样本数据,基于统计的算法总有局限性,特别是在泛用性上不够(无法适配未知场景),因此我们还需要结合专家的规则来做算法结果的校准和降噪,专家规则的沉淀和执行就需要一个低代码研发的平台,当时我把它称为 MaaS(Monitoring as a Service,所谓监控即服务)。将数据查询、算法、可视化等等能力都 API 化,并在低代码平台里提供,方便专家编写各种各样的规则。
随着这样思考的形成,以及后续接触到外部的一些场景,特别是工业领域、物联网领域以及金融方面,随着软件和硬件的传感器入侵到各种各样的产品和业务里,海量产生的时序数据的处理、存储和分析,会非常需要上述这样的产品及能力。
当然,我们不是去复制过去做过的的系统,而是基于上述的思考,在企业上云以及云原生的大潮下,重新思考整体的设计和架构,从头去做一套更极致和优秀的产品,这样的产品不仅是可以服务于中大型的客户,并且可以灵活地以标准化甚至 SaaS 的方式服务中小客户,将我们拥有的丰富实践服务到更广大范围的用户。
从更大的范畴来看,中国互联网在中国这个复杂、丰富并且规模巨大的业务场景下,已经锤炼出不少过硬的技术,我们也有非常优秀的工程师,是可以去参与全球的市场竞争,只是在产品化、UI 体验、国际化以及对用户需求的更细致挖掘上面,还有很多值得提升的地方。
告别稳定的、有保障的生活并不容易,不过我在加入大厂之初就想过,下一份工作要么就是创业,要么就是一干到底了。转眼之间已经是奔四的年纪,回忆当初成为一个程序员的初衷是创造一个世界级的软件,我距离这样的目标仍然非常遥远,我仍然热爱编写代码,仍然对世界拥有一份好奇心,在和即将创业和已经创业的朋友聊过之后,痛并快乐的体验吸引着我。我从来不是一个完全理智的人,否则也不会在 12 年离开淘宝加入 LeanCloud 创业,况且,一个理智的人应该不会在这个年份去创业吧,哈哈。在萌生这样的想法并且不可抑制之后,我也找到了志同道合的伙伴,说干就干。
从 4 月份离职到现在,公司的成立,团队的构建,产品的设计和研发,这个过程有太多值得总结和回味的地方,有机会再细细分享。这个过程中得到许多人的帮助和信任,心里充满感激。我自己一个不成熟的感受就是,创业其实是将你过去一段时间的积累重新梳理、挖掘并且充分发挥的过程,创业其实是从很早就开始的。同时,创业的过程仍然需要去注重知识、人脉和技能的积累,否则总有一天会把过去的积累掏空,你就很难去面对未来的挑战。
既然是我们说了算的公司,我们当然不想再去做一家我们“讨厌”的公司。我们希望公司的氛围是开放、透明,充满信任和鼓励,简而言之,它必须是一家常识意义上的好公司。常识我们都懂,什么是好人,你有一些基础的判断,什么是好公司,大家也有基础的判断。回归到常识,很多事情的价值判断是一目了然的。但是好,不意味着是和稀泥,不是老好人,而是在充分的、透明的沟通下,大家对做事的目标、方法达成一致,有一致的追求和品味,并且对这些目标有严格的定义,完成的过程和结果有严格的要求。
我们也希望做一个好产品,它不能只是我们说好,它必须真正能帮助到客户,解决客户的痛点,同时,它必须有好的文档和 UI/UX 体验,它也应该是国际化,因为作为开源的 Infra,它天然就应该是面向全世界的。它需要是实用的、解决问题的、酷的、有品味的(P.S. 我觉得我们的 Logo 和网站设计都挺有品味,哈哈)。这当然非常非常难,不过却是我们想去追求的目标。
开源是一开始既定的策略,原因也很简单,我们几个人都热爱开源,也从开源学习和吸收了大量的知识,我们想回馈开源社区。作为创业公司,做基础类的软件,开源也是一个很好的策略,国内这几年涌现出来优秀的开源软件公司也给了我们极大的启发和信心。不过做开源,我们想第一天代码就按照最终开放的标准去要求,无论是 code style 的规范、commit message 的规范,还是研发分支管理和 PR 合并的规范、issue 和文档的规范等等,我们从写下第一行代码就逐步去完善和执行,我们也希望未来所有的功能里程碑的制定、方案的设计以及完整的研发流程,也能以开放、透明的方式在开源社区里呈现,也欢迎志同道合、感兴趣的朋友来玩。
(我们在一起等将研发了半年的项目 repo 公开)
(Repo 公开那一刻的 Github 机器人消息截图)
2022 年,我们顺利将 GreptimeDB 开源,也开始基于这个 OpenCore 构建我们的产品,团队也初步磨合成形。我自己又开始大量地参与研发,Github 的热度也起来了,虽然跟团队里的优秀的年轻人没法比,哈哈。年轻人一定是越来越优秀的,如果不是,那一定是哪里出了问题。
2023 年从任何一方面来讲,都将是更具挑战性的一年,宏观层面不会很乐观,但是打铁还是需要自身硬,我们需要去走一遍技术到产品,产品到商品,以及商品走向主流市场的路,这对我和团队来讲,都是没有完全走过的路(尽管我们在 LeanCloud 已经经历了部分)。不过既然已经扬帆起航,无论前方是风平浪静的碧海晴天,还是疾风暴雨的惊涛骇浪,也只有妥善掌舵,勇往直前!
GreptimeDB: https://github.com/GrepTimeTeam/greptimedb
Posts
有无之间
我们对于“拥有”总有一种执念,这可能是动物的本能,从婴儿时期就很明显的表现出来,这个玩具是我的,这个好吃的是我的。这种本能是自然界教育出来的,没有毛病。 昨天不知道为何,突然思考起拥有的真正含义,得出一个结论,我们并没有真正拥有。很巧,晚上跟朋友吃饭,也聊到这个话题。拥有本质上只是一个假象,某个物件在你手中,也只是暂时的拥有,它终有一天会转手,会损坏,会丢失,会蒙灰,它跟你的缘分也就那短短的几年甚至几天几个小时。我们和父母、妻儿、亲友的羁绊,也只是这一世,你并没有真正地拥有你的儿子,你的妻子,他们有自己的想法,思考,独立的人格。我们能够拥有的都只是这短短数十年的陪伴、关爱和争吵。
其实这个想法并不新鲜,跟佛家所说的缘聚缘散是一个道理。只是更进一步的思考,如果一切东西都是短暂拥有,是不是不值得去奋斗?不值得去珍惜?我经常跟同事说要有 ownership 的意识,是不是在忽悠人?答案是否定的。让你不执着于“有”,不表示你应该放弃“有”,不去奋斗,不去珍惜,不去坚持。相反,正因为天下没有不散的筵席,我们更应该珍惜所“有”,活在当下,不悔过去,不忧未来。
简而言之:恰到好处。增之一分则太长,减之一分则太短。这个度的把控在于心,而按照王阳明的说法心即理,不可外求天理,你要做的是时时刻刻去私心,行的是为善去恶的功夫。最高的境界可能就是孔子说的从心所欲不逾矩了。
Posts
这是一个招聘帖
加入蚂蚁金服还有 4 个月就满两年了,这一年半来了,还是做了一些很有意思的事情。现在在 twitter 分享比较多,还有关注我博客的朋友可以关注下。
从今年开始,我们小组总算成型了,目前专注在做时序数据库方向,这个时序数据库有应用在蚂蚁内部的监控、数值指标分析等场景里,落地了很多场景,今年预计会有一个更大的场景来落地,面临的技术挑战很大。除了海量的数据写入压力之外,对于成本和性能也提出了更高的要求。如果仅仅是时序,想象空间仍然是有限的,我们还在开拓更多的应用方向。除了存储之外,我认为现在其实我们已经收集了非常多的数据,但是这些数据的价值还没有充分挖掘,为了从这些数据中抽取有价值的信息,无论是使用传统大数据的分析方法,或者使用 AI 机器学习来挖掘,都需要数据库提供一套可供分析计算的框架出来,而这一块的挑战就更大了。
我们的技术栈是 golang + rust + java:
Rust 用于编写数据库的存储引擎,我们把它当成更安全的 cpp 在使用。总体而言, rust 除了刚开始的学习曲线稍微陡峭点以外,熟悉之后的开发效率还是可以的,而性能优势比之带 GC 的语言来说就更大了。我们选择 rust 来写引擎的原因,除了看重语言和平台带来的性能红利之外,更重要的原因是使用底层的系统语言,你才能更好贴近硬件,利用好硬件的能力,特别是现在“新”硬件的发展。隔着一个虚拟机,你能做的事情总是受限的。 我们使用 Golang 做集群的分布式架构,类似 gateway 和分片、容灾调度等集群管理工作,随着计算方向的进一步清晰,未来可能还会使用 Golang 做类似计算调度的事情等。 最后,我们用 java 在做大数据分析这块,大数据分析引擎目前还是以 java 居多,作为更直接接触用户的方向,适配公司内的主流语言是必须的,另外团队内也有很资深的 java 工程师也是一方面因素。 我们这个小组不大,目前还不到 10 个人,也是我喜欢的风格,做事情靠谱,氛围简单,所以下面是一个广告时间。
Rust 引擎研发工程师 CeresDB 作为高性能、低成本并具有实时分析能力的新一代分布式时序数据库,已经应用在蚂蚁金服主站、网商和金融云等诸多场景中。我们面临着海量数据存储、访问和分析的技术挑战,急需对此方向感兴趣的朋友加入。
你将参与蚂蚁金服时序数据库 CeresDB 存储引擎的研发,包括但不限于:
核心存储引擎的研发。 存储与计算分离架构的研发。 引擎层面对于实时计算和分析框架的研发。 软硬件结合技术的探索和应用。 岗位信息:
层级:p7~p9 工作地点:杭州/北京 我们对你的要求:
工作三年以上,有存储引擎研发经验者优先。 计算机基础扎实,熟悉常用数据结构和算法,掌握 C/C++/Rust 语言中的至少一门,愿意学习和使用 Rust 语言。 熟悉 linux 系统,有丰富的系统编程经验者优先;有良好的编码习惯,追求卓越的工程实践,易于沟通,并具有持续学习的能力。 对于时序数据库领域有所了解和研究的优先,有系统性能调优经验者优先。 Golang 研发工程师 你将参与蚂蚁金服时序数据库 CeresDB 的研发,包括但不限于:
Posts
昨天、今天和明天
这题目非常俗套,不过同名的本山大叔的小品我却是很喜欢,哪怕很多人说有歧视的嫌疑。
其实这篇博客就是吹吹水,总结下我过去 5 年做了哪些微小的工作,为什么加入蚂蚁金服以及加入蚂蚁金服准备做什么。
2012 年我从淘宝离职加入了 AVOS 中国公司,很多人看来不理解,为什么放着到手可得的股票而去加入一家前途未卜的创业公司?其实很简单,当时我在淘宝呆了三年,一直做消息中间件相关的工作,先是 Notify(蚂蚁这边叫 msgbroker ),后来是发现了 kafka,所以有了 MetaQ。。以当时的眼光看,MetaQ 很多东西都是更强大,功能更全面,比如高可靠的复制方案、事务消息乃至支持 XA 协议的分布式事务。但是做久了,确实腻歪,然后去转岗,最后半年去做 TAE(Taobao AppEngine)项目,第一次接触和引入了 Storm,当然,更重要的是结识了 Clojure 这门语言。顺其自然,国内玩 clojure 的人这么少,就认识了现在 LeanCloud 的 CEO 江宏,在杭州咖啡馆聊了一次,没过多久,我就决定加入了。所以说原因很简单,一是我想换个环境,当时的眼界和心境决定了我没法继续在阿里呆下去,我想离开工作了三年的熟悉环境,换一门新语言,换一个完全不同的工作环境,认识不同的人,尝试下创业是什么感受。二是江博士的个人影响力,我认同他的许多观点,也庆幸能参与到他组建的这个优秀团队。
如果以这些原因来看,我的目的完全达到了, AVOS 中国办公室当时是一个非常牛逼的团队,我认识了很多远比我优秀的工程师,无论是国内的,还是国外的,有一些现在还保持着很好的联系。我经历了数个产品的从生到死,并且全程参与到最终 LeanCloud 这三年来的成长和发展。这 5 年来我也基本一直在用 clojure 写代码,不敢说完全精通,但是至少可以吹牛逼地说,我是国内少数的几个有丰富 clojure 实践经验的工程师。更重要的是这 5 年来经历的一切,对我来说是完全全新的体验。创业非常艰难,同样,创业也非常激动人心。
AVOS 中国团队经历了好几个产品,美味书签开始,我们先是想尝试做 delicous 的中国版本,加上社交和设计类的元素。但是总所周知, delicous 已经死了。我这个阶段还是主要在后端,做一些文本抽取算法、后端服务之类的工作,也帮助 delicious 基于 Solr Cloud 做了新的搜索系统,解决数亿文档的搜索问题。在美味书签失败后,类似无觅这样的基于兴趣 tag 订阅的阅读类开始兴起(其实今日头条也是),我的同事孙宁开始主导美味爱读的开发,基本是参照着 Prismatic 来做,我又参与进去,记得是做后端 API 和分类算法,第一次接触了聚类推荐算法相关的东西。美味爱读真的是非常好的产品,可惜他的原型 Prismatic 也死了,不过他们留下很多美妙的 clojure 库,比如 schema。在美味爱读的开发的同时,另一拨同事做了玩拍这个短视频项目,因为当时短视频 Vine 非常火,我们“又”赶热潮去做了玩拍。玩拍的做的还是算是比较成功,火过一阵子,但是很可惜,仍然是没有坚持下去,加上没有持续的内容运营,更重要的是团队上大变动,反而当时对标的秒拍坚持到了现在。玩拍第一个版本的后端 API 是我设计和实现,后续因为参与美味爱读和 Delicious 重构,就退出了这个项目。然后同事朱老板一直很关注 parse.com,认为移动大潮下,这个东西还是有价值的,说要不我们做一个试试。LeanCloud 就这样横空出世。很夸张,但是却是现实, AVOS 中国公司的末期有三个产品在齐头并进。
Posts
Erlang 调度器初探
最近读了《Characterizing the Scalability of Erlang VM on Many-core Processors》, 刚好轮到我做技术分享,就做了个 PPT 总结了下:
Slideshare 链接: http://www.slideshare.net/killme2008/erlang-scheduler 。
文中的 ping 测试代码 ping_pong.ex。
Erlang 的 VM 是 c 语言写的,PPT 中给了一些源码链接,下一步准备去仔细探下这个迷宫。
Posts
自旋锁的优化
自旋锁的提出主要也是为了解决多核系统的同步问题。当锁要保护的代码块执行很快的时候,并且争抢不是非常激烈的时候,自旋锁的比之重量级的需要切换上下文的互斥锁能带来更好的性能表现。对于非多核系统,用户态的自旋锁空耗 CPU,反而降低了整个系统的 progress,作用反而不大。
此外,当保护的代码块执行较为耗时,或者自旋锁的争抢非常激烈的时候,自旋锁本身就消耗了大量无谓的 CPU ,这种情况下还不如使用互斥锁,让出 CPU 给任务执行,提高实际的 CPU 利用率。
我在前面一篇介绍 CPU 高速缓存的博客,举了个例子用 AtomicInteger 的 compareAndSet 实现一个自旋锁,主要是为了演示在 SMP 下通过增加一个内循环,来减少锁状态 state 在多个 CPU 之间的『颠簸』。
但是其实这个例子如果用 synchronized 或者 ReentrantLock 改写,都会快得多。比如换成 ReentrantLock,
private ReentrantLock lock = new ReentrantLock(); /** * 递增 count,使用 ReentrantLock 保护 count */ public void incr() { lock.lock(); count = count + 1; lock.unlock(); } 运行时间下降到了 150~230 毫秒,比之原来测试的两个版本快了一个数量级。
原因就在于递增 +1 这个操作非常快,导致自旋锁版本的代码线程争抢锁非常激烈,大家抢的头破血流,但是任务却没有多大进展,空耗了大量 CPU。就好像一堆人抢着去上卫生间,大家都不排队,你挤我抢,反而堵在了门口,卫生间的实际利用率下降了。从这个生活中的例子出发,我们可以想到一些优化的方法,比如分散每个人的争抢时间,比如排队。
因此,自旋锁的优化有一些思路:
首先是退让(Back off),每个线程在争抢锁之前等待一小会,通常第一次不退让,但是之后就按照某个规则变化这个退让时间,我们修改下原来的 BusyLock 例子来演示这个优化,这里采用指数退让:
//最大退让时间 10 毫秒 private static final int max_backoff_msg=10; /** * 利用 CAS 实现自旋锁 */ private void lock() { //其实退让时间 1 毫秒。 int backoff=1; while (!
Posts
分布式一致性论文阅读阶段性小结
这个月阅读集中在分布式一致性和存储方面。
首先是 Paxos 系列论文:
《Paxos Made Simple》,循循渐进地讲解 paxos 解决的问题、逐步增强的约束条件(P1、P2、P2a - P2c)等,P1 保证至少有一个值被接受, P2 保证只有一个被选中的值被所有 process 接受。然后介绍两阶段的步骤:
Phase 1. (a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
* (b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered pro- posal (if any) that it has accepted.
Posts
Join Idle Queue 负载均衡算法解析
JIQ 是微软发的一篇论文《Join-Idle-Queue: A Novel Load Balancing Algorithm for Dynamically Scalable Web Services》里描述的负载均衡算法,这里总结下我所理解的内容。
背景 负载均衡很常见,比如我们经常用 nginx 做反向代理和负载均衡,Nginx 也支持了 weight、ip_hash、url_hash 等均衡算法。
负载均衡的图示:
任务 jobs 不停地经由多个 dispatcher 转发给后面的 processor server 处理。
dispatcher 选择哪一条 processor 来转发任务的过程就是 load balance 的核心问题。尽量降低任务的响应时间是我们的目标。
最简单的算法可能是随机或者轮询,但是这种简单的策略会造成响应时间的最大化,特别是高负载的情况下。
更优化的策略有:
JSQ: Join-the-Shortest-Queue,每次将任务加入最少任务队列的 server。这就要求 dispatcher 收集每个 processor server 的任务队列大小信息,但是随着 dispatcher 本身的集群化以及云服务厂商的大规模应用,这个收集产生的网络通讯就更加膨胀了。 SQ(d)(Power-of-d):每当任务到达的时候, dispatcher 就随机取样 d 个 processor 服务的队列大小,选择最小任务队列的那个派发。通常 d 选择为 2。这个算法相比随机算法能带来响应时间指数级别的提升。但是仍然需要在分发任务的时候获取 processor 队列信息,这个同步调用在任务派发的关键路径上,对性能有很大影响。 工作窃取和共享:工作窃取就是空闲队列主动去从其他任务队列『窃取』任务,或者繁忙的队列主动将任务『推送』给其他空闲队列。这个算法更适合共享内存的系统,对于 web 负载均衡,在不同后端 server 之间做任务的窃取或者推送会带来额外的开销和复杂度,想象一个 web http 请求如何转交到另一台后端 server,涉及 TCP 链接的迁移和请求的同步等等。 JIQ 全称就是 Join-Idle-Queue,它的提出就是为了解决大规模 Web Services 的负载均衡问题。
Posts
Clojure Under a Microscope(1):Clojure 如何理解代码(下)
继续上篇,本篇的目的是将 parse 过程介绍完成,然后我们就可以进入编译和运行环节。
目录:
LispReader 补充 Analyze Parser 举例:if Parser 举例:fn FnMethod Primitive 参数性能优化 总结 上篇在介绍 LispReader 源码核心片段的时候没有介绍最后一个比较关键的代码片段:
String token = readToken(r, (char) ch); return interpretToken(token); interpretToken 方法将去解析字符串 token 的含义,token 就是一个词汇单元,它的含义是什么,完全由 interpretToken 决定:
static private Object interpretToken(String s) { if(s.equals("nil")) { return null; } else if(s.equals("true")) { return RT.T; } else if(s.equals("false")) { return RT.F; } Object ret = null; ret = matchSymbol(s); if(ret != null) return ret; throw Util.runtimeException("Invalid token: " + s); } 代码其实很清楚,token 可能是:
Posts
微信 phxpaxos 源码解读:fsync 和 fdatasync
最近在读微信开源的 paxos 实现 phxpaxos,读到 localstorage 部分学习到 fdatasync 系统调用。这一部分是非常核心的存储模块,参与者的状态信息、变更日志等都要写入磁盘并且可能要求强制刷入存储磁盘避免系统崩溃等情况下数据丢失,性能的很大一部分因素取决于这一块的实现。
phxpaxos 使用了 LevelDB 做存储,但是做了几个优化:
LevelDB 存储的 value 只是一个 24 个字节的 fileid 索引(有一个例外 minchosen 值),真正的状态数据存储在他们自己实现的 log_store 里, fileid 存储了在 log_store 里的文件编号、写入 vfile 的offset 和 checksum 这三个字段信息。这样做的原因是由于 leveldb 对于比较大的 value 的存取效率不是最优,通过间接一层存储,利用了 LevelDB 的 Key 的有序性和小 value 存储的性能优势,加上定制的 log_store 的写入优化,达到最优组合。 log_store 按照文件编号固定增长,比如 1.f、2.f、3.f ……以此类推(在日志目录的 vfile 目录下)。并且每个文件都是在创建的时候分配固定大小,默认 100M(还有所谓 large buffer 模式分配的是 500M)。值的写入都是 append 操作,写入完成后,将偏移量、校验和、当前文件 ID 再写入 LevelDB。读取的时候就先从 LevelDB 得到这三个信息,然后去对应的 vfile 读取实际的值。因为文件是固定大小分配,每次强制刷盘就不需要调用 fsync,而是采用 fdatasync,这样就无需做两次刷盘,因为 fdatasync 不会去刷入文件的元信息(比如大小、最后访问时间、最后修改时间等),而 fsync 是会的。 一张图来展示大概是这样: