本文翻译自 jemalloc Postmortem，原作者 Jason Evansjemalloc 内存分配器 最早构思于 2004 年初，并且已经公开使用了大约 20 年。由于开源软件许可的性质，jemalloc 将会永久保持公开可用，但是上游的活跃开发已经结束。本文简要描述 jemalloc 的开发阶段，每个阶段都有些成功或失败的亮点，最后附有一些回顾性的评论。Phase 0: 源于 Lyken2004 年的时候，我开始在科学计算的背景下开发 Lyken 编程语言。这门语言最终挂了，但是它的手动内存分配器到 2005 年 5 月已经功能完整。（不过，原本打算在其上实现的垃圾收集器从未完成）2005 年 9 月，我开始将分配器集成到 FreeBSD 中，到 2006 年 3 月，我移除了 Lyken 中的分配器，转而使用系统分配器的一个简单封装版本。为什么在投入了这么多努力之后，还要移除从 Lyken 中集成的内存分配器？其实我们刚把这个分配器集成到 FreeBSD 中，就发现其实系统分配器所唯一缺少的就是一个统计、跟踪内存分配量的功能，目的是能够触发线程级别垃圾回收。而这可以通过使用线程特定数据和 dlsym(3) 的简单包装来实现。有趣的是，许多年后 jemalloc 添加了 Lyken 当初缺少的统计收集功能。Phase 1: 进入 FreeBSD时间回到 05 年，FreeBSD 当时拥有 Poul-Henning Kamp 设计的卓越的 phkmalloc，但是这个分配器没有提供多线程环境的合适规定，而 Lyken 的分配器看起来是一个明显的改进，并且颇具可扩展性。在朋友和同事的鼓励下，我把它集成了过来——很快，人们开始把它称作 jemalloc。但这进展有点儿太快了——集成后不久，我们就明显发现 jemalloc 在一些负载下存在严重的碎片化问题，尤其是由 KDE 应用程序引起的那些。就在我以为 jemalloc 的迁移差不多胜利了的时候，这个现实场景中的失败让 jemalloc 的可行性受到了质疑。简要来说，碎片化问题源于使用统一的范围分配方法（即没有大小类分离）。我从 Doug Lea 的 dlmalloc 中获得了基本灵感，但没有采用那些经过实战检验的交织启发式算法，这些算法避免了许多最严重的碎片化问题。随后我们在这方面疯狂搞研究和实验。到 jemalloc 成为 FreeBSD 版本的一部分时，其布局算法已经完全改变，改为使用大小分离的区域，如 2006 年 BSDCan jemalloc 论文中所述。Phase 1.5: 整合 Firefox到了 07 年11月，Mozilla Firefox 3即将发布，高度碎片化是一个未解决的问题，尤其是在微软的 Windows 上。因此，我们花了一年时间与 Mozilla 合作处理内存分配问题。将 jemalloc 移植到 Linux 很简单，但 Windows 就是另一码事了。jemalloc 的官方源代码在 FreeBSD libc 库中，所以我们基本上分叉了 jemalloc 并添加了可移植性代码，将任何与 FreeBSD 相关的代码都推送到上游。整个实现仍然在一个文件中，这减少了分叉维护的困难，但肯定是在这个阶段中的某个时间，整个实现的复杂性已经超出了单文件所能承载的范围了。多年后，Mozilla 开发者对上游 jemalloc 做出了重大贡献，以摆脱他们需要自己维护一个专门分支的处境。但不幸的是，Mozilla 的基准测试一直显示，他们的分支版本优于上游版本。我不知道这是否是过拟合的问题，还是说性能上存在实际的差异，但这仍然是我对 jemalloc 留有的最大遗憾之一。Phase 2: 在 Facebook在 2009 年加入 Facebook 工作后，我惊讶地发现阻碍 Facebook 基础设施中广泛使用 jemalloc 的最大障碍是监控。关键的内部服务处于一种尴尬的境地：我们依赖 jemalloc 来控制内存碎片，但工程师们需要使用 tcmalloc 和 gperftools 中的pprof堆分析工具来调试内存泄漏。因此兼容pprof的堆分析功能就成了 jemalloc 1.0.0 版本的重中之重。jemalloc 开发迁移到了GitHub，接下来几年中，随着不断出现的问题和机遇，开发断断续续地进行着。其他开发者开始贡献重要的功能。我们在 3.0.0 版本引入了广泛的测试基础设施，以及对 Valgrind 的支持。4.x 系列引入了基于衰减的清除机制和 JSON 格式的可观测性能力。5.x 系列为了更好地与 2MiB 的巨页交互，我们从块（chunk）迁移到了块范围（extent）。在某种程度上更具争议的是，我在 5.0.0 中移除了对 Valgrind 的支持，因为它在维护上非常麻烦（在许多细节处缠绕不清），而且在 Facebook 内部并未被真正使用——其他工具如pprof和 MemorySanitizer 占据了主导地位。我几乎没有收到过关于 Valgrind 支持的反馈，因此推断它并未被使用。其实回顾起来，情况似乎并非如此。特别是，Rust 语言直接将 jemalloc 集成到程序中，我认为 Rust 开发者和 Valgrind 开发者之间存在一定的重叠。人们对此感到愤怒。jemalloc 可能被加速排除在了 Rust 二进制文件之外。（译者注：这是说移除了对 valgrind 的支持后，Rust 如果继续使用 jemalloc，加上 valgrind 进行分析时就玩不转了）Facebook 内部的遥测令人叹为观止，拥有来自众多服务的性能数据来指导内存分配器开发是一大福音。过去十年中速度最快的两个内存分配器（tcmalloc 和 jemalloc）都受益于这些数据，这并非巧合。即使像“简单”的快速路径优化，在有汇总的 Linux perf 数据的情况下也更容易做对。像避免碎片化这样的难题仍然很难，但如果数千个不同的工作流程表现良好，没有异常回归，那么改变大概率就是安全的。jemalloc 作为 Facebook 基础设施的性能、弹性、一致性支持的一部分，受益匪浅。此外，jemalloc 自带的集成统计报告功能天生能够匹配这些常规的监控需求，并且我们最终发现这通常对 jemalloc 开发和 Facebook 以外的应用程序调优/调试的益处远超过实现它所付出的努力。在我 Facebook 的最后一年，我被鼓励组建一个小型的 jemalloc 团队，以便我们能够处理一些原本令人生畏的大任务。除了主要的性能改进之外，我们还实现了 CI 测试和全面的遥测。当我 2017 年离开 Facebook 时，jemalloc 团队在尊敬的同事 Wang Qi 的领导下，基本上没有我的参与下，已经连续进行了好几年出色的开发和维护工作。正如提交历史所证明的那样，这里面还有许多其他人的卓越贡献。Phase 3: 变为 Metajemalloc 开发的性质在 Facebook 重新更名 Meta 之后明显发生了变化。Facebook 基驾减少了对核心技术的投资，转而更加注重投资回报。这在 jemalloc 的提交历史中表现得非常明显。特别是，关于原则性的大页分配（HPA）的种子早在 2016 年就已经播下了！然而 HPA 的工作持续了几年，然后逐渐放缓，最终因为缺乏必要的重构来保持代码库健康，各种调整调整互相叠加，进入了停滞的局面。这一功能轨迹最近出现彻底崩溃了。对我来说，这种失望之情在某种程度上被钝化了，因为我已经多年没有密切参与其中，但最近 Meta 内部的变化导致我们不再有人能够长期指导 jemalloc 的发展，以期实现普遍的实用性。我不想过多讨论戏剧性的事情，但也许值得提到的是，尽管大多数参与者都是出于善意，但 jemalloc 在 Facebook/Meta 的手中最终还是走到了一个悲伤的终点。企业文化会根据内外部压力的变化而变化。人们会发现自己陷入了两难的抉择：要么在极端压力下做出糟糕的决定，要么在极端压力下屈从，要么干脆被绕过。作为个人，我们有时有足够的影响力来减缓组织的退化，甚至可能为某种孤立的复兴做出贡献，但没有人能够逆转这些悲剧发生的趋势。我非常感激我的前同事们在 jemalloc 上所做的出色工作，也感谢 Facebook/Meta 长期以来的巨大投资。Phase 4: 停滞现在怎么办？就我个人而言，“上游” jemalloc 的开发已经结束了。Meta 的需求很久以前就已经不再与外部使用的需求很好地对齐了，他们最好自己做。如果我要重新参与进来，第一步至少需要几百个小时的重构来偿还累积的技术债务。而我对之后的前景并不足够兴奋，不愿意支付如此高的前期成本。或许其他人会创建可行的分支，无论是从 dev 分支还是从 5.3.0 发行版（已经三年前的版本了）！在前面的部分中，我提到了几种特定阶段的失败，但在专注于开源开发的整个职业生涯中，我还是对一些一般的问题感到惊讶：正如前面提到的，移除 Valgrind 当时引起了不少负面情绪。但问题的根本在于对外部使用和需求缺乏意识。如果当初我知道有人关心这个问题，我可能会与其他人一起努力保留 Valgrind 的支持。另一个例子是，我大概有两年时间完全不知道 jemalloc 在 Android 操作系统中作为内存分配器的使用情况。多年后，同样在不知情的情况下，jemalloc 被替换了。尽管 jemalloc 的开发完全公开（并非在Facebook内部封闭），该项目从未发展到保留来自其他组织的主要贡献者。由 Mike Hommey 驱动的，Mozilla 推动 Firefox 迁移到上游 jemalloc 的努力功亏一篑。其他人尝试过渡到基于 CMake 的构建系统的努力多次受阻，也从未完成过。我从与 Darwin) 经历过的艰难经验中认识到，内部封闭的开源项目无法繁荣（HHVM是一个重复的教训），但 jemalloc 作为一个独立项目要繁荣，需要的不仅仅是开发层面的开放。jemalloc 对我来说是个奇怪的转变，因为我超过 25 年来一直是垃圾回收的强烈支持者。我个人很高兴再次投入到垃圾回收系统的开发中，但 jemalloc 是一个非常令人满足的项目。感谢所有使这个项目如此有价值的人，包括所有合作者、支持者和用户。
答案是：不太行。（必须得手动编译，参考enable-postgis-extension-for-postgresql-16-on-mac-m2）所以本文是标题党。其实我要说的是如何切换到 pg14 并让 postgis 正常跑起来。首先我们在 pg16 中会遇到如下问题：postgres=# CREATE EXTENSION postgis; ERROR: extension "postgis" is not available 描述: Could not open extension control file "/opt/homebrew/opt/postgresql@16/share/postgresql@16/extension/postgis.control": No such file or directory. 提示: The extension must first be installed on the system where PostgreSQL is running.这个问题的原因是 postgis 没有把插件所需的内容正确放到 pg16 所需的地方，那么我们需要手动 copy 一下：cp /opt/homebrew/Cellar/postgis/3.4.3/share/postgresql@14/extension/* /opt/homebrew/opt/postgresql@16/share/postgresql@16/extension/然后再启动就会发现版本不适配：postgres=# create extension postgis; ERROR: PostGIS built for PostgreSQL 14.0 cannot be loaded in PostgreSQL 16.4 背景: PL/pgSQL function inline_code_block line 15 at RAISE这个时候我们重新下载一个 pg14 就可以了，不同版本的软件在 homebrew 中可以同时存在：brew install postgresql@14 brew services stop postgresql@16 brew services start postgresql@14这样就可以正常在 pg14 当中使用了：postgres=# create extension postgis; CREATE EXTENSION postgres=# SELECT ST_Touches( ST_GeomFromText('LINESTRING(0 0, 1 1)'), ST_GeomFromText('LINESTRING(1 1, 2 2)') ); st_touches ------------ t (1 行记录)
全职做 Apache Doris 开发的第三年，也是带社区同学参与 Apache Doris 开源项目的第三年。过去一年里我带了大约十多位同学参与了开源项目，获得了实际的产出，也有两位同学进入了 SelectDB 全职进行 Apache Doris 的项目开发。这件事情老实讲对我个人并没有什么裨益——它不构成我的工作产出，项目也不归我所有。但是在有限的时间内，我还是希望能够回馈到社区。通过 good first issue 的方式来链接在校生的一小部分，和业界的一小部分。你的大部分疑问会在下面得到解答。做什么首先需要介绍一下什么是 Apache Doris，以及社区的运行机制。Apache Doris 是一个分布式的实时 OLAP 数仓，关于它的详细介绍你可以在网上随处找到。简单来说，Doris 是目前国内性能最佳的 All-in-One 数仓和 OLAP 引擎。除此之外，和你比较相关的是，作为一个开源商业化项目，它有着现在罕见的社区活跃度。这指的是它不仅接受几个核心贡献组织的开发，而且对于自发参与的个人开发者非常友好。作为 Apache 的开源社区，项目不从属于商业公司，而是 Apache 基金会，它依基金会的规章而行事。但是实际的维护基本上依赖一些背后的商业公司。作为开源社区，我们将几乎所有开发内容公开贡献出来，与很多你耳熟能详的国内大公司一起共建，包括腾讯、百度、京东、美团等。这其中也不乏个人开发者。而 good first issue 就是个人开发者参与开源的良好第一步。什么是 Good first issue？这指的是一部分较为简单，且涉及模块较少，比较独立的开发任务。对于新手来说，完成这种任务的开发不需要太多知识和复杂的 debug 过程。对于社区来说，这种较为简单的工作交给同学们可以有助于我们专注于更重要且复杂的主线任务。这样同学们可以从中掌握经验、快速成长，社区也不必花费过多精力解决一些琐碎问题，这是一个绝对的双赢。于是现在许多开源社区都有了 Good first issue。这些任务都会有专门的 mentor 带领你入门。为什么做这个问题换句话说就是，我能从中开源开发中得到什么。参与开源项目，构建软件世界的梦想和成就感——如果你认同这一点，那么这就是你的答案了。但是对大多数同学来说这可能比较虚，所以我们需要说下面几点：一个超过绝大多数“项目经历”的“项目经历”。现在 CS 越来越卷，就我个人看到的（过去一年我看了50-100份 DB 相关的校招简历），什么 15445 6.s081，以往的闪光点近乎成为标配。但开源至今为止还没有。几乎所有简历上的项目都不如你实际参与过大型开源项目的经历。这是一个绝对的闪光点大型项目协同开发的经历。在你完成代码的过程中，你会经历很多非常实际的问题，例如 IDE 的、git 的。这些东西是你个人开发几乎遇不到的角落，但它们很重要。如果你没有相关经历，那么在下一段工作中你需要在这上面花费不少时间。当你提交 PR 之后，你会接受社区其他成员的 review，以我的经验，代码至少需要修改 4-5 次。这个过程中你会意识到，很多理所当然的东西，其实需要反复推敲斟酌。当你解决掉这一切问题，你的能力已经到了一个新的台阶。谁适合参加首先，good first issue 绝不是什么困难的东西，但对于全职员工的产出来说 ROI 可能偏低，但对于没有相关经验的同学来说它又能极大地丰富自己的简历和就业条件，所以从这个角度来说，应当是：在校生等缺乏大型项目实际经验的人——这对各位来说有很大帮助另外对于项目本身来说，我们需要：能快速上手的人。这些任务都比较简单，有大量可以参考的例子，需要你有充足的时间，能快速去做，然后解决实际的问题并达成目标。需要什么对于 good first issue 来说，基本上不需要什么技能。常见的编程语言如 C++ 和 Java，熟悉它的基本语法即可。如果有什么不会的，你可以在使用中学到它们。但有一些技能是你需要掌握的：学会借助 GPT 解决问题。你会遇到很多和命令行、git 等有关的问题，其中绝大多数都可以在正确描述的前提下用 GPT 解决，省时省力。参考例子。你参与的绝大多数任务都会有极其相似的其他例子，请先参考并尝试理解他们。沟通。如果遇到问题，首先尝试自己解决，如果确实无法解决，feel free to communicate。准确描述你的问题上下文，会更容易得到解答。在整个开发的过程中，你需要时不时和 mentor 同步你的进展。动手能力。快速去做，而不是说。好了，以上问题都是针对参与开源的泛泛之谈，接下来是你如何参与到 Doris 开发之中的步骤，也基本上是我写这篇文章的原因——这样大家就可以来这里看到一些必须的内容，节省了我复制粘贴的时间。环境搭建首先我们需要一台 8C 16G 以上的 linux 系统的开发机。linux 物理机最佳，虚拟机和 MacOS 也是可以的。考虑到 8C16G 这个配置在当今较为常见，所以对你来说最好的方式是用你现有的 Linux 系统（如果有的话）或者直接用虚拟机和 WSL。如果实在没有条件，可以联系我给你开社区公共开发机的账号，但那个机器开发体验比较差劲。然后你就需要去搭建开发环境了，这里你需要参照以下文档：首先使用 git 拉取 Apache Doris项目然后看这里：使用 LDB Toolchain 编译——你可以参照这个文章准备好编译环境然后按照手动部署存算一体集群这个文章部署你的编译产物并进行验证当然，在开发的过程中你需要使用好你的 IDE，可以参考例如Apache Doris Be 开发调试在内的许多文档。包括如何使用插件进行 Debug，也有相关的文档进行描述。当你验证完你编译部署的整个流程都没有问题，那么就可以联系我，给你分配对应的开发 issue 了！哪里找到 case如果你希望自己去寻找一些 case，也是可以的。社区有对应的 issue 标签，例如：good first issue 和 doris-future，带有这些标签的意味着是给社区同学开放的开发任务。如果它还没有assigner，或者太久没有活动迹象，那么请大胆评论，了解任务情况，并申请认领这个任务！然后你就可以和你的 mentor 开始沟通，着手开发了！当然，我们社区还有一些长期的大型计划，这些计划也都需要大家的共建，例如impl all command in Nereids，这里我们已经把一个大型计划切分为了一些小的 issue，且里面有非常多已经完成的类似 issue 可以参考。你可以随时 pick 它们。这里我们有详细的指南：成为 Apache 顶级项目贡献者之路：Apache Doris 的语法迁移攻略。你随时可以参与建设它，估计这个 Project 到 25 年下半年才有机会完工。upd：鉴于社区同学的热情，我们专门开放了一个专项 [[Good First Issue] Support All SQL Functions in Other SQL System](https://github.com/apache/doris/issues/48203) 出来，这里主要是各类函数的支持工作，非常适合 newcomers 来尝试。原因是：函数支持的任务涉及的上下文比较确定，不需要太多的对项目的认知，且有我制作的非常详细的指南和历史的大量参照 PR，是很容易实现的。整个过程涉及 BE、FE、添加单测和回归，非常有助于熟悉 Doris 的整个开发流程。而且，对于其中的大部分任务，在完全照猫画虎的工作之外，还需要你开动一定的脑筋把性能、可读性做到最佳水平。整个学习曲线非常适中。如果想联系我，那么在这个函数专项下面留言，我会回复你。在这之后……？当你参与了足够多的开源项目之后，你会感觉到自己已经有了足够的能力去 cover 一些独立的问题，带着这份能力去找工作或者实习，会事半功倍。如果你有兴趣继续全职参与 Apache Doris 或者其他数据库产品的开发，可以联系我，我会给你提供合适的机会。当然，在那之后你或许也有机会成为 OSPP 和 GSOC 的参与者！将在开源世界承担起更大的责任。另外，在这里我会挂一些我知道的、容易参与的、蓬勃发展的其他开源项目，有兴趣的同学可以在之后适时去了解，它们是：xmake——取代令人难受的 CMake 的新一代构建工具，已经小有名声Apache Kvrocks——同样对新贡献者非常友好的一个分布式 RocksdbTiDB——知名国产数据库，很早就开始了友好的社区协同开发计划Apache Dubbo——一款原生微服务框架，也有持续的 good-first-issue 发布等。
背景近日笔者在为 Apache Doris 支持字典表功能，主要专注于 FE 的管理模块和优化器相关的查询计划生成。期间有一个 bug 由于笔者对 GSON 的机制毫无了解，花费很长时间未解决，遂尝试使用 Windsurf(调用模型为 Sonnet)进行分析。结果令笔者大为震撼，现记录如下，或许对当前大模型在大型 codebase 下能力边界的认知，有一定帮助。问题这里有一个核心类 Dictionary，它继承自 Table，who has 成员 rwLock。这个 Dictionary 和基类在现版本都是通过 GSON 序列化完成持久化的。也就是为名字添加了 @SerializedName 注解，然后利用GsonUtils 完成 read 和 write 对象。基类 Table 中， rwLock 对象在每一个构造函数中都进行了初始化。然而问题来了，我的 FE 在重启之后，Dictionary 对象通过基类拥有的 rwLock 却总是失效的。也就是说序列化与反序列化出了问题。public class Dictionary extends Table { @SerializedName(value = "dbName") private final String dbName; @SerializedName(value = "sourceCtlName") private final String sourceCtlName; @SerializedName(value = "sourceDbName") private final String sourceDbName; @SerializedName(value = "sourceTableName") private final String sourceTableName; ...... @Override public void write(DataOutput out) throws IOException { String json = GsonUtils.GSON.toJson(this); Text.writeString(out, json); } public static Dictionary read(DataInput in) throws IOException { String json = Text.readString(in); return GsonUtils.GSON.fromJson(json, Dictionary.class); } } public abstract class Table extends MetaObject implements Writable, TableIf, GsonPostProcessable { protected MonitoredReentrantReadWriteLock rwLock; public Table(TableType type) { this.rwLock = new MonitoredReentrantReadWriteLock(true); ... } public Table(long id, String tableName, TableType type, List<Column> fullSchema) { this.rwLock = new MonitoredReentrantReadWriteLock(true); ... } ...... }Sonnet Works接下来通过多轮问答，Sonnet 阅读代码库并不断产生猜测，并由我来验证。让我们看看它做得如何。Question 1我的 @Dictionary 继承自 @Table.java ，在Table的每一个构造函数中rwLock都会被赋值，但是我在将一个Dictionary作为TableIf执行.readLock()却报错rwLock为null，请浏览上下文以及任何可能的有关文件，帮我尝试找到这是为什么Answer 1让我帮你调查这个问题。首先，我需要查看Dictionary类和Table类的实现来了解继承关系和rwLock的初始化情况。首先让我查看Dictionary类：Analyzed code item Dictionary.java:Dictionary现在让我查看Table类：Analyzed Table.java:(1-101)让我继续查看Table类的其他部分：Analyzed Table.java:(101-201)从代码中我发现了一些重要信息：Table类中的rwLock是在构造函数中初始化的，有两个构造函数：Table(TableType type)Table(long id, String tableName, TableType type, List fullSchema)在这两个构造函数中都有初始化rwLock的代码：this.rwLock = new MonitoredReentrantReadWriteLock(true);从Dictionary类的代码中可以看到，它确实继承自Table类，并且在其构造函数中调用了父类的构造函数：public Dictionary(CreateDictionaryInfo info, long uniqueId) { super(uniqueId, info.getDictName(), TableType.DICTIONARY, info.getColumns().stream() .map(DictionaryColumnDefinition::getOriginColumn).collect(Collectors.toList())); // ... }让我查看一下Dictionary是如何被序列化和反序列化的，因为这可能是导致rwLock为null的原因：Searched in fe/fe-core/src/main/java/org/apache/doris/dictionary 1 result我发现问题了。在Dictionary类中，我看到它使用了GSON进行序列化和反序列化：@Override public void write(DataOutput out) throws IOException { String json = GsonUtils.GSON.toJson(this); Text.writeString(out, json); } public static Dictionary read(DataInput in) throws IOException { String json = Text.readString(in); return GsonUtils.GSON.fromJson(json, Dictionary.class); }这就是问题所在：rwLock是一个非序列化字段（没有@SerializedName注解），这意味着它在序列化时会被忽略。当使用GSON反序列化时，它会创建一个新的Dictionary实例，但不会调用构造函数，而是直接设置那些带有@SerializedName注解的字段。由于rwLock没有被序列化，且在反序列化时没有调用构造函数，所以rwLock保持为null。解决方案有两个：在Dictionary类中实现GsonPostProcessable接口，并在gsonPostProcess方法中初始化rwLock：@Override public void gsonPostProcess() { super.gsonPostProcess(); this.rwLock = new MonitoredReentrantReadWriteLock(true); }或者修改序列化/反序列化逻辑，不使用GSON，而是手动序列化所需的字段，并在反序列化时调用适当的构造函数。我建议采用第一种方案，因为它更简单且不需要改变现有的序列化逻辑。这样在GSON反序列化后，会自动调用gsonPostProcess方法来初始化rwLock。你想要我帮你实现哪种解决方案？Question 2但是看起来 @OlapTable.java 和 @Table.java 的gsonPostProcess也并没有对rwLock进行处理啊？为什么就没问题呢Answer 2让我检查一下Table和OlapTable的序列化和反序列化逻辑：Analyzed Table.java:(501-601)让我看看Table的read方法：Analyzed Table.java:(401-501)我明白了问题所在！在Table类中，有两种反序列化的方式：在版本136之前（Env.getCurrentEnvJournalVersion() < FeMetaVersion.VERSION_136），使用readFields方法，这个方法会创建一个新的Table实例并初始化rwLock。在版本136及之后，使用GSON反序列化：return GsonUtils.GSON.fromJson(Text.readString(in), Table.class);而Dictionary类覆盖了read和write方法，总是使用GSON进行序列化和反序列化：@Override public void write(DataOutput out) throws IOException { String json = GsonUtils.GSON.toJson(this); Text.writeString(out, json); } public static Dictionary read(DataInput in) throws IOException { String json = Text.readString(in); return GsonUtils.GSON.fromJson(json, Dictionary.class); }这就导致了Dictionary总是使用GSON反序列化，而GSON反序列化不会调用构造函数，所以rwLock没有被初始化。解决方案：Dictionary类应该遵循和Table类相同的版本控制逻辑，在版本136之前使用readFields，之后才使用GSON。或者在Dictionary的gsonPostProcess方法中初始化rwLock：@Override public void gsonPostProcess() throws IOException { super.gsonPostProcess(); if (this.rwLock == null) { this.rwLock = new MonitoredReentrantReadWriteLock(true); } if (this.commitLock == null) { this.commitLock = new MonitoredReentrantLock(true); } }我建议使用第一种方案，让Dictionary遵循和Table相同的版本控制逻辑，这样可以保持代码的一致性。你觉得哪种方案更好？Question 3但是我出现问题的version超过了VERSION_136，是在现在最新的版本上发生的，这个推论对吗？Answer 3你说得对，让我重新检查一下。如果是在VERSION_136之后，那么Table类确实也是使用GSON进行反序列化的：} else { // VERSION_136及以后 return GsonUtils.GSON.fromJson(Text.readString(in), Table.class); }这意味着Table类在新版本中也是通过GSON反序列化的，它应该也会遇到同样的rwLock为null的问题才对。让我检查一下Table类是如何处理这个问题的。Searched in fe/fe-core/src/main/java/org/apache/doris/catalog 22 results我注意到Table类和一些子类（如OlapTable、View、EsTable等）都实现了GsonPostProcessable接口。让我看看这些类是如何处理反序列化后的初始化的：Analyzed Table.java:(1-101)让我看看Table类的gsonPostProcess方法：Analyzed code item Table.java:gsonPostProcess啊，我发现问题了！你说得对。在VERSION_136之后，Table类确实也是使用GSON反序列化的，但是它的gsonPostProcess方法并没有初始化rwLock。这意味着rwLock在反序列化后应该也是null才对。让我看看其他表类型是如何处理这个问题的：Analyzed code item OlapTable.java:gsonPostProcess这很奇怪。OlapTable和其他表类型也没有在gsonPostProcess中初始化rwLock，但它们似乎都能正常工作。让我再检查一下Table类的构造函数是否在某个地方被调用：Searched in fe/fe-core/src/main/java/org/apache/doris/catalog 36 results我明白问题了。我之前的推论是错误的。在VERSION_136之后，Table类确实使用GSON反序列化，但是GSON的反序列化机制会调用类的无参构造函数。这意味着：当使用GSON反序列化时，它会先创建一个对象实例（调用无参构造函数），然后再设置序列化的字段。Table类是一个抽象类，它没有无参构造函数，但它的子类必须有。Dictionary类继承自Table，但它没有提供无参构造函数，这就是问题所在！解决方案： 在Dictionary类中添加一个无参构造函数：public Dictionary() { super(TableType.DICTIONARY); }这样，当GSON反序列化Dictionary时，会先调用这个无参构造函数，这个构造函数会调用Table的构造函数，从而正确初始化rwLock。这也解释了为什么其他表类型（如OlapTable）能正常工作 - 它们都有无参构造函数。你想要我帮你实现这个解决方案吗？总结Java 对于 cppboy 来说确实过于 magic 了，它需要无参构造函数，而当我不提供的时候并不是报错，而是提供非预期的行为。我想这也是所谓”面向约定编程“的一部分吧X...Xb，可惜我对 GSON 的约定一无所知，but Sonnet does!我觉得让我比较惊讶的是，在啊，我发现问题了！你说得对。在VERSION_136之后，Table类确实也是使用GSON反序列化的，但是它的gsonPostProcess方法并没有初始化rwLock。这意味着rwLock在反序列化后应该也是null才对。这段上下文附近。Sonnet的确在尝试验证自己的猜想，并且在猜想与其他背景不符的时候进行反思，而不是出现”幻觉“。这证明目前的这套工具链(Windsurf/Cursor + Sonnet/4o)在独立解决问题方面，也许的确行得通。
Authentication plugin 'mysql_native_password' cannot be loaded 解决方案
使用 mysql 客户端登录时候报错，问题发生实际原因是 mysql9.0 不再默认包含 mysql_native_password 插件了。它的密码哈希存储方式被证明是不可靠的。采用更新的可靠验证方式，如 caching_sha2_password，需要连接的对端进行支持。如果对端支持，那么 mysql 协议握手时可以自行确认。但是如果你要连接的服务端不是 mysql 且不支持新的认证协议，那么就需要回退 mysql 版本。在 Linux 下安装一个老版本客户端即可。如果是 MacOS 那么可以这样方便地执行：brew install mysql-client@8.4 brew unlink mysql brew link mysql-client@8.4
Let's encrypt 官网)并未提供 CentOS 的傻瓜式安装步骤，这里我们主要参考这一篇安装之后使用这样的命令生成证书给 Nginx 使用：sudo certbot certonly -d yourdomain.com -d *.yourdomain.com --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory中间每一步按提示操作即可。会需要为 DNS 配置 TXT 记录。成功以后 Certbot 会提示证书的生成目录，查看一下：[root@xxx nginx]# tree xxx/zclll.com/ xxx/zclll.com/ ├── cert.pem -> xxx ├── chain.pem -> xxx ├── fullchain.pem -> xxx ├── privkey.pem -> xxx └── README其中 cert.pem 为公钥，chain.pem 为中间链证书，fullchain.pem 为二者组合。privkey.pem 为私钥证书。因此在 Nginx 的配置文件 nginx.conf 中，为对应 server 做如下配置即可：ssl_certificate = fullchain.pemssl_certificate_key = privkey.pem然后重启服务即可正常运行。续签证书通过 --manual 方式生成的证书是无法通过 certbot renew 自动更新的（因为中间需要手动完成一些操作，就像我们刚完成过的一样），除非通过 --manual-auth-hook 提供一个交互式的脚本。简单起见我们可以选择通过certbot certonly --standalone命令手动更新。选择 renew 即可。
最近几年德州扑克（Texas Hold'em）在国内流行度有显著上升，尤其是在IT和金融行业。很多人在玩了一些手牌之后，会逐渐思考自己的打法，尝试从一名纯娱乐玩家转变为一种尝试真正理解这个游戏的业余玩家。那么这篇文章就是为你而准备的。德州扑克当中有两个核心的概念，或者说，仅有两个核心的概念。一切的手牌发展和玩家博弈都是围绕着“位置”和“范围”进行的。接下来我们依次讲解：位置位置指的是你在当前轮次的座位，假设我们有一个七人桌，那么位置从小盲起依次是SB、BB、UTG、MP、CutOff、Hijack、Button。当你刚开始接触德扑，你可能关注自己手中的牌有多好（绝对牌力），以及在翻牌后有没有中到牌。但当你进行了足够多的游戏，你必须明白——在不同的位置进行游戏，你可以玩儿的手牌，以及下注的方式都是完全不同的。1如果你在前位，比如说，枪口位，拿到了一对口袋2，你是否应该入池呢？你会觉得都中了对子，无论如何也不是翻前应该放弃的牌，所以你选择入池。那么当你平跟入池（limp）以后，你期望后位的对手们会如何行动呢？比如说，他们有人做了很大的开池（open-raise），然后还有人做了3-bet，现在轮到你了，你会怎么做呢？这些主动操作的对手之中，是否有人会拿着比你大的口袋对呢？你不知道，但这很有可能，对吧？在绝大多数的情况下，你的这手牌带来的唯一效果就是，直接损失了自己limp的筹码。因为你完全无法反击后位玩家们的操作。他们可能有更大的口袋对，然后推回来3-bet甚至4-bet，你傻眼了。现在你需要选择扔进去更多筹码面对自己几乎不可能有利的翻牌，或者接受自己刚刚的limp打水漂。如果你硬着头皮跟注了，那么让我们看看翻牌之后会发生什么，假设是AKQ这种牌面，那些open和跟注很大尺度的人，他们会一个人都没有中牌吗？这显然不太可能。即使是78J这种排面，你也不知道会不会有人中牌了，他们只要有人中牌，就会比你大。那么接下来的故事就是，你会弃给对手的任意一个操作。你看，你的一个平跟入池，在完全不知道后位玩家会怎么做的情况下，把自己陷入了一种非常尴尬的境地。大多数时候，面对会玩的对手，你只能被对手的加注剥削，白白损失自己的跟注（相比于上头去继续跟注看翻牌，这反而算是好事儿了）。[scode type="blue" size=""]简单的思考：既然这种操作会让人如此难受，那我在后位的时候是否应该多多益善地加注或诈唬，让前位对手陷入尴尬境地？[/scode]2那么你同样拿着一对口袋2，但你是Button位，前面的玩家要么过牌，要么弃牌，现在到你行动，你可以如何操作？显然你知道前面的玩家没有足够大的牌，那么你的对2是不是已经是最强牌力了？如果你希望更少的人看到翻牌以便赢下这手牌，你可以轻松的下足够大的注，在必要的时候，甚至ALL IN也是一种选择。[scode type="yellow" size=""]为什么Allin都可能是一种选择，但limp仍然不是？[/scode]又如果前面的玩家操作的非常激烈，打出了3bet、4bet，仍然很多人跟注，你该怎么办？这个时候，我相信是一个标准的easy fold。因为你知道了比在前位所能知道更多的信息，你猜到了对手可能是些什么牌，所以你的决定总是“更正确的”。这就是位置的重要性。你现在知道了，在不同的位置，即使是相同的牌，采取的行动和获胜的机会也是截然不同的。所以，在前位频繁地平跟入池，也绝对不是一个好的策略。这恰恰是新手最常犯的错误，他们总是想要看一眼翻牌，然后再根据是否中牌决定是否下注。但是后位有经验的玩家会告诉你——看翻牌是要有代价的。通过上面的例子很容易理解，先于对手做决策，和后于对手做决策，会面临完全、完全不同的处境。这本质上是“信息”的重要性。除非你拿着那些超强的牌，例如AA、KK，否则在翻前的位置，比你手里的牌，更有资格决定你应该如何行动。范围德州扑克是一种概率游戏。所谓概率，指的并不是运气。运气是指，这个游戏我有30%、50%或者70%的概率赢下来，但我归根结底得听天由命，就像彩票一样。而概率是指，任何玩家在任何一手牌上都有可能输或者赢，但如果你打得比桌子上其他玩家好，那么当手牌数足够多，你就会长期较稳定地必然盈利。（也就是我们所说的期望收益，EV）这种概率，本质上就是来自于德州扑克的核心——“范围”。什么是范围，事实上当你打过几手牌，你就已经意识到了——如果对方在翻前领打，在翻后继续c-bet，每一条街上都领打，那么不需要别人告诉你，你就知道，对面要么是真的有很强的牌，要么至少是在扮演很强的牌（bluff，诈唬）。你对这名玩家“要么牌力很强，要么正在尝试诈唬”的认知，就是我们所谓的“范围”。我们前面所说的，在更后面的位置行动，相比于前位行动的优势，就是在于对对手范围的掌握。在前位下注，你面对的是“全范围”对手，而在偏后的位置的下注，你已经知道前面的玩家更大概率拥有偏强还是偏弱的范围。范围基于两点，一个是玩家的历史形象，一个就是他的行动线。这也是德扑的核心。德扑要赢，本质在于：知道自己是什么牌知道对手是什么牌知道自己的牌很容易，你只要不要“误认”自己的牌就好了（玩家会误认自己的牌吗？我们会讲到的）。那么如何知道对方的牌呢？你可能已经有所感觉：通过行动线。行动线[scode type="green" size=""]玩家的每一次行动，都是在调整他在其他玩家眼中的范围。[/scode]——所以，我们必须善用这一点来做决策。这又会是一个很好的例子，说明新手的经典limp入池是不好的：当你在翻前拿到了一对10，如果前位没有人open，那么你应该open入池，而不是limp。因为你的操作，会带来对手的行动，而对手的行动，说明了他们的范围。这些信息是帮助你赢游戏的关键。你如果做了足够大的open入池，翻牌后235彩虹面，那么你就知道——这些跟注的玩家不太可能得到牌力的增强——因为2、3、5这种牌，如果他们拿着对应的手牌，在翻前不太可能跟的下足够大的open，对吧？这就是你翻前做了正确的open得到的回报——调整了对手的范围。你现在知道那些和你一起看翻牌的人，他们有可能会因为转牌、河牌出现Ace、King之类的牌而得到增强，而235这种牌足够令你放心。因此你可以去做c-bet，拿到更多的价值。而且open还有一个重要作用，就是隔离范围。[font color="BlueViolet"]一个很简单的数学道理——一个人中牌反超你的概率是20%，那你就有80%的机会赢他。而五个这样的对手入池，你要赢下底池的概率就只剩32%了。[/font]如果你翻前并没有open，大家一起来看到翻牌，那就糟了。天知道会不会有人手里拿着对2、对3，或者能够中顺子的46o呢？你现在举步维艰，不知道是否应该c-bet。如果你c-bet被对手推回来，你通过什么去猜测对手是不是46o呢？答案是没有办法——翻前的软弱此刻得到了惩罚。这是一个最简单的例子，告诉你行动线和范围之间的关系。一个翻前主动3bet、4bet的人，他的范围会是“有一些大牌或者不小的对子”，一个小的翻牌面对他没有什么帮助。范围能帮助你解决很多问题。如果一名玩家每条街上都在领打，那么干燥面上，河牌如果出一张无关的小牌，他是很难扮演自己中牌了的——因为他的范围里根本就没有这张牌。（所以诈唬需要“扮演”某种手牌，要达到这种合理性是很困难的，不仅要求自己的形象，也要求牌面给自己机会——这也是“听牌转诈”技术的基础）历史形象而历史形象，我们一般从“松-紧” “弱-凶”两个维度进行建模。我们说一名玩家是“松”的，意味着他会拿着更大范围的牌进行游戏。而我们说一名玩家是“凶”的，意味着他更不容易弃牌，且会有更多激进的下注。在GTO打法出现之前，几乎所有的世界一流玩家都是“松凶”型玩家。这很好理解——因为只有这样才能拿到更多的价值。而对于业余玩家来说，“紧凶”是最容易盈利的类型。因为你的对手往往是容易弃牌、且容易被洞悉的。只要拿到那些该拿的价值，做出那些容易的弃牌（fold），就足以保证你的盈利了。这也是为什么，德扑中的手牌不能轻易让他人看到。因为它明确地告诉了对手，你的一部分范围信息。当对手看到了足够多你在什么情况下会弃牌、在什么情况下会跟注、加注，那么对手甚至不太可能做出错误的决策。如果一个人的形象足够清晰且不够平衡（简单理解为，具有某种鲜明特征），那么剥削他是很容易的。比如，新手有可能遇到一个人，以很高的频率在翻前用大注码偷盲，那么正确的行为是什么呢？是被欺负很多次之后，热血上头拿着某一手牌，把命运交给荷官吗？显然不是的。这种情况下，对手的历史形象告诉了你，他的范围不够平衡。具体而言，他的Allin和重注里面，必然混有了太多的诈唬和听牌。那么在这种时候，你就可以用更宽的范围去应对对手的下注然后去惩罚他的范围缺陷——这仍然是基于你的手牌和对手的历史形象，而不是基于你的忍耐力。当你手牌还没有击中你认为合适的“更宽的反击范围”的时候，轻松地弃牌没有什么可耻的。[collapse status="true" title="被剥削的本质是什么？"]这也回答了一个可能的疑问：对手总是打得很凶，拿各种各样的牌去open和c-bet，让我不得不弃牌或者很难受的跟注，我该怎么办呢？让我们这样去思考这个问题。每个人都有自己的范围，按照这个范围去行动，那么最终谁会赢，谁会输，由什么决定呢？答案是范围本身是否足够“平衡”。如果一个人诈唬过多，那么我只要在有足够的抓诈牌力的时候去抓诈就好；如果他从不诈唬，我就很容易在一些潮湿牌面上弃牌止损。如果他总是打得很凶，那么我应该害怕吗？恰恰相反，如果你是平衡的，那么说明对方的加注范围太大了，你能拿更宽的范围去对抗他、惩罚他。如果你觉得对方打得很凶，但又不是随便诈唬，而是刚刚好让你做了很多hard disicion？那么更有可能的是，范围不够平衡的那个人是你。你正在被对手“合法剥削” XD当两个人范围不对称的时候，总有一方会盈利（EV，也就是期望收益，为正，而另一方为负），这取决于谁是更为“平衡”、不容易被抓住漏洞（也就是明显不平衡的倾向）的那个。[/collapse]不过，有一种情况是必然出现的。就是你可能做了很多自认为正确的决定，但你还是在某几手牌上快速输光了全部的筹码。这说明你做错了什么吗？我们后面会谈到的。当然，现代的德州扑克产生了基于混合策略纳什均衡的GTO打法，它的核心是利用透明信息的纳什均衡。这就不在新手进阶所需要考虑的范围之内了。如果你进阶到了半职业、职业的水准，那么你自然会考虑是否使用那些GTO Wizard之类的东西。你能赢什么牌？德州扑克最根本的盈利逻辑，是“拿价值”。我们前面说了，每名玩家在他人眼中都拥有自己的“范围”，且随着历史形象和当前行动线变化。但还有一种情况，叫做：“比你小的不会跟，跟了的都比你大”（也有人叫做“找爸爸”行为），新手很容易产生此类错觉而做出错误决定，我们举个例子。翻前你在枪口位拿A7s Open，后面打出了3bet、4bet，你跟注到翻牌，此时翻牌圈是A、K、3彩虹面，你击中了一个顶对Aces。这是好事儿吗？看起来是的，顶对理论上讲不会是什么弱牌力，但仔细想想，事情是这样的吗？你的后位会拿什么样的牌去打出3bet、4bet呢？对方会没有Ace吗？对方会没有King吗？你现在中了顶对，但你能赢谁？此时你会发现，你的牌非常尴尬，你只能赢那些口袋对。因为你们范围中的牌力得到了同等的增强。你不可能把赢牌的希望寄托在对方拿着9Ts这种牌打到4bet进来看翻牌，对吗？就像我们一开始所说的那手口袋2一样。你如果跟注了一个翻前打的很激烈的多人底池，什么牌会一直玩下去，且比你弱呢？似乎并没有。你变成了一手注定在这里输掉筹码的抓诈牌（bluff-catcher）。所以你就更应该问问自己：我在前位拿A7s这种牌去跟注4bet看翻牌，我希望看到什么呢？然后你会发现，你潜意识里以为的那些outs（我有一张A，所以我如果中A了就好，不中A就弃牌），其实是为你而设的陷阱。不要被“绝对牌力”所迷惑，你永远要关注这些重点中的重点：对手现在范围里有哪些牌是会支付给我的，有哪些牌是我需要支付的？我如果下注xx%，对手的跟注范围会变成什么？会有什么牌跟注进来，且是我能赢的？[font color="BlueViolet"]你觉得自己牌力很强，但是你下注了，那些比你小的牌很容易弃掉，你隔离出来的都是那些比你大的牌来call，那就搞笑了。[/font]做正确的决定除了位置和范围以外，或许有一点更重要的，就是坚持做正确的决定。德扑是一个概率游戏，意思是，每一手牌的结果都是不确定的。对手就是有可能乱打，有可能用完全不平衡的打法，有可能天天诈唬，然后你抓诈的时候恰好抓到真正有牌的时候。So what?反思决定，而不是结果当你打过足够多手牌，就应当遇到过无数次这种情况——翻前弃掉了78o，然后翻牌圈看到了778，然后懊恼不已：我要是刚才跟注了就好了！但是，刚才跟注了，真的就好了吗？你在翻前随时拿任何牌入池，都有可能遇到中大牌的情况，比如22直接变成金刚薄纱对面的Aces，但这对吗？我们随时可能做着正确的决定输掉一手牌，也可能用错误的决定赢得盆满钵盈。但你不是只玩这一手牌，对吧？如果你未卜先知，看得出下一张牌是什么，那这当然是下错注了。但你没有（我们先假设你没有出千XD），那么从概率意义上来说，你就是对的。Allin了，你有60%的概率赢下这手牌，40%的概率输掉，结果你输了。所以这个Allin是错的吗？显然不是。你永远不必反思一个正确的决定，即使它让你输光光。你只需要反思那些：对手打得我很难受，是不是我的范围不够平衡导致被剥削了？亮牌以后发现对手比我大，我是否估错了对手的范围？资金管理做好资金管理是必须的。你不能把德扑从一种娱乐变成一个赌博游戏。做好资金的预算，什么时候升级，什么时候降级，都是你开始之前的必修课。比如，你随时准备着30个买入进入当前级别，那么——也许是心态，也许只是连续几天的运气问题——当你连续输了很多个买入以至于自己的资金总量不足，你就需要立刻给自己降级。把它当做一个数学题来处理，严格执行一些既定的规则，否则它就会毁掉你的生活。总结当然，对于新手来说，德州扑克还有很多需要注意的点，比如马脚（tells）就是自己不容易意识到的。未经专门训练的人，在拿到大牌的时候，总是有一些小动作的，哪怕最基本的思考时间不同，也会给他人带来很多信息。但相比于其他无数个细节，是否能理解德扑是关于“位置”和“范围”的游戏，决定了一个牌手是否进入了上升的正确通道。本文所说的所有情况，你都可以考虑它的反逻辑。也就是说，我可以用很强的牌，通过小注、过牌扮演弱牌引诱对方以获得更大价值（强牌慢打）；或者用很弱的牌，去扮演一手强牌，通过对手的弃牌获利（诈唬，Bluff）。但如果你因为刺激或者“出其不意”而想要增大自己这些操作频率的时候，请先问问自己：我在该赢的牌上是否已经拿到了该拿的所有价值？我是否能够克制住自己，不在应当弃牌的情况下赌运气而被对手剥削？当你理解了“位置”和“范围”，你会很快需要意识到，“范围”有两个不同的视角。一个是通过行动线和历史形象，你和对手能够互相推知对方的范围，从而德扑不再是一个完全的运气游戏；而另一点是，既然我的行动线会透露我的范围，这会给对手带来相当大的决策优势（easy decision），那么我是否应该用反逻辑去盈利/避免被剥削？当我连续c-bet会让对手很容易的弃牌，那么我的听牌、空气牌是否也可以扮演这样一手牌？这就是你必然的下一课了——范围平衡。当你意识到这一点并需要精进了，那么这篇教程的后续也许就会发布。不过，不要在基础的问题还没有解决的时候，尝试平衡你的范围。你会把3%-5%的冷门操作，很容易玩成30%-50%的频率，导致平白无故的损失。
这篇文章用来解决两个问题：如何更新操作系统的时区文件，让它得知目前最新的夏令时信息（这会依当局的行政命令而发生改变）如果遇到了时区信息错误，如何解决第一个需求最为常见，然而第二个问题对于 CentOS 用户来说同样有一定意义。下次有其他倒霉蛋遇到这个问题的时候，就能从本文确定问题和修复方式了。问题当你使用的应用不自己维护时区信息时，他们会从 Linux 系统的 tzdata，也就是系统的时区数据库读取这些信息。这一般依赖$TZDIR指示的路径（因此你可以通过这个环境变量修改它——如果你的应用程序足够聪明，知道先看看这个变量的话），如果没有指示，那么默认是/usr/share/zoneinfo目录。如果你的场景强依赖正确的夏令时数据，那么你必须经常更新这个数据库。因为很多主体的夏令时每年并不一致，而这依赖于当局的行政命令，因此你需要根据新的公告数据去更新，以保证你的软件和现实世界表现一致。如果你使用 CentOS，在某些涉及到时区的场景中，发现涉及 Africa/Casablanca（卡萨布兰卡）这个时区的数据总是不对，而其他绝大多数情况都是正常的，那么大概率是因为：这儿的 tzdata 本来就是坏的。如何解决你需要做的是更新系统的时区文件到一个正确/最新的版本（取决于你的需求）。接下来是过程：首先你需要下载 IANA 时区数据库，这里我们就用最新的，其他也很容易在 ICANN 找到：wget https://www.iana.org/time-zones/repository/tzdb-latest.tar.lz将其解压，然后完成 build 过程，这可能需要 C 编译环境：tar -xf tzdb-latest.tar.lz cd tzdb-2024a/ make TOPDIR="$HOME/tzdir" install # 决定编译到哪里 "$HOME/tzdir/usr/bin/zdump" -v America/Los_Angeles # 读一个文件验证一下然后你在TOPDIR给定的路径就得到了一份新的 tzdata 及相关文件。接下来需要用它们去覆盖本系统中的文件（如果需要，记得备份）：sudo cp -r $YOUR_PATH/usr/share/zoneinfo /usr/share这就足够更新时区数据了。当然如果你需要更全面的更新，包括一些时区相关工具，那么你可以把TOPDIR所有的文件拷贝并覆盖到/目录对应位置下。参考资料Africa/Casablanca timezone missing #10211Africa/Casablanca timezone issue #1442作者在 SelectDB 的内部文档
[scode type="blue" size=""]开拓视听，颇具参考意义。[/scode]语言——JS，yyds！（Rust长势喜人——越难找的员工，越值钱啊——Rust，唯一原神——这么多的人在用C++做游戏？这么少的人用C++写数据库吗。。职业生涯——学历？——内生的兴趣，是计算机行业的独到优势啊——好吧，这个比例远超我的想象AI——毕竟，这是一个需要知识和智力的行业——确实，谁又爱写测试呢数据库——interesting~ redis已经作为八股融入了cn后端人的血液了。另外，在这里好像大家不那么愿意接受新鲜事物（指pgsql——Navicat，CN特供！（破解版）开发者——嗯。。入职即高级！——码农是青春饭（——看起来很有信服力——呃。。。——青春饭和老龄化社会——实际润人的数量开发工作——woo，相当高的比例C++——比我们想象的要快，不是吗？——哈哈，大家都很想要modules，但是谁又能完整支持呢？——好吧，这么多人不写单测——我们需要（而且没有）一个合理的解决方式——GCC远胜于CLANGRust——新兴力量，不是吗？参考文献以上全部数据，来自JetBrains每年进行的开发者生态调查，原文刊于https://www.jetbrains.com/zh-cn/lp/devecosystem-2023/
[scode type="blue" size=""]以下内容均来自南方周末编辑部[/scode]20年变迁，不言自明。2024，新年贺词——守住不惑的底线，选择做最值得的自己2024年的第一束阳光正在深处积蓄，我们——生于1984年的南方周末也即将迎来创刊四十周年。按照最新一次人口普查的结果，中国人的平均年龄也大约四十岁。 “四十”而“不惑”，前者是这份新闻纸与吾国吾民的微妙同频，后者是我们念兹在兹的自我期许。 我们诞生于改革开放初启鹏程的春天里，现实百废初兴、百舸争流，未来充满未知、诸多困惑。也正因如此，穿越惑与不惑的边界，求索东方古国现代文明进步的答案，内蕴于我们的基因与使命。 四十年来，我们寻刻着一个民族走向复兴的经纬坐标，记录着无数个体命运的跌宕起伏；我们追求真、歌颂善、呈现美，我们抗拒假、斗争恶、暴露丑；我们持续见证、诉说并努力参与那场壮丽的春天的故事，又从那个故事中汲取无穷力量，让我们在面对未知与困惑时，绝不背离自我的基因与使命：在这里，读懂中国。 读懂永无止境，不惑则是一种状态。本应不惑的年岁，依然会强烈感受到各种困惑的推背感。 百年变局仍在高速演变，一个2023年就足以令人头晕目眩： 暴雨一路向北，杜苏芮狂卷乱奔，气象气候变幻莫测；支原体肺炎上热搜，流感成热词，南北儿科问者如云；网约车多地饱和，全职儿女等待“上岸”；谁记得住3000点保卫多少回？谁忘得了朱令与琴相伴又与病魔抗争的故事？摘下口罩的那一刻，你是否感到寒气？ 中美元首在大国关系的十字路口晤谈“正确相处之道”，俄乌冲突未了，巴以厮杀又起，中东局中局风云变幻，福岛核污水每一秒钟都在随洋流扩散，这个地球，能否和合与共？ ChatGPT取代的是马还是马车夫？算法会控制你还是解放你？人类的未来是不是马斯克的星辰大海？新技术带来的是元宇宙的美梦还是潘多拉的魔咒？ 惑与不惑永远在刷新边界，行动是穿越的唯一途径。越是目眩神迷，越要抱朴守一，在沧海桑田中守护方寸安宁殊为珍贵。 同在2023年，人们走向烧烤摊，走进演唱会，来一场“特种兵”式的旅行，在City Walk中与远方破镜重圆；与逝者道别，向生者微笑，为村超欢呼，为电竞喝彩，自己出演短剧的主角，让曾长出荒草的街道变成cos的魔法舞台。 这是变局中人的日常，毕竟唯一不变的只有变化本身。而在这其中，守住自己的内心，守住自己的生活，就是在守住不惑的底线，守护人生的大盘。 这也是似曾相识的过往。四十年来，我们看到无数这样的你，我们不停为你加油，因为我们就是你们的一部分。 我们感受着你的困惑，更感佩于你的行动。我们知道你生于平凡，也明白你心有不甘；我们看到你徘徊踯躅，也欣赏你迈开脚步；我们明白你犹豫不决，也理解你做出抉择；我们目睹你只有单枪匹马，也见证你汇成千军万马。 每一次何去何从的困惑，都可能通向一场毅然决然的醒悟。即使不知道答案，即使不清楚前路，仍可选择做最值得的自己：去思考、去行动，去迎接、去探索。无论游刃有余还是头破血流，每一次闪展腾挪都是一次饱含希望的奋起，仿佛每一团在冬夜升起的篝火，都在为迎接春日而燃烧。 不惑是有进无退的前行，是心怀牵挂的闯荡，是背着大山的跳舞。是上有老下有小肩上扛着整个世界却无可依靠的“必须坚强”。那就为自己点个赞吧，然后咬着牙继续走。 不惑是不确定中的结构涌现，是危机中萌发的生机。是在陷入迷茫时坚信自己的珍贵，在遭受厄运时坚持自己的志趣，经过光阴淘洗的种子，破土就是风华。 不惑是从心底生出光与热。即便历经创伤，也以明媚暖意，驱散焦灼与阴翳，随身携带一个宇宙，把爱的能量场安放在自我和他人之间。 不惑是既直击本质，又不失本真。是看清真相后的依然热爱，是洞穿世事后的心怀慈悲，是人生智慧的集中绽放，是“勿意、勿必、勿固、勿我”的笑对。 不惑是哪怕见惯山川湖海，仍能在意一粒沙、一滴水。是千疮百孔里依然藏着的倔强与柔软，是星夜兼程时对一轮朗月不知所起但一往情深的眷念。 不惑是时刻关照弱者的声音。我们每个人都可能是弱者，而社会的安全感与幸福感，也来源于让每个自强的人，能有示弱的机会。不惑的灵魂是夜空中最亮的星，既照亮自己的前路，也成为别人的明灯。 不惑是坚定那些被历史验证的共识。直面复杂与跌宕，始终执手相行，始终心怀敬畏，始终敢于创造。 不惑是坚守“正义、良知、爱心、理性”，是“让无力者有力，让悲观者前行”，是“没有一个冬天不可逾越”，是“总有一种力量让我们泪流满面”，是“每一个这样的你都是英雄”。 而不惑的本源，终将回到真实。每一道困惑的目光里，都闪烁着一个真实的梦想，那可能是大城市里的一张床，是地震后的一碗牛肉面，是父母的养老钱，是孩子的课间十分钟，是一则没有“仅限35岁以下”的招聘启事。每一次踏实的求解，都源自一个不被遮蔽的问题，敲开真相上的坚壳，方能触摸答案。 亲爱的读者，新年的第一束阳光正待升起。这缕晨曦将无远弗届，驱走长夜阴冷，照进大地每一个角落，照进过去、现在与未来。在南方周末将满四十周岁之际，我们期待能与你共同做到不困、不惑、不忧、不惧。 祝你新年快乐！2003，新年贺词——“全面小康”与“公正社会”当我们的目光从注视了一年的黄土地、黑土地、红土地上抬起时，新年的阳光已经第十九次炫目地照在中国这份平凡的报纸上。19年来，我们一次次、一年年，艰难而执著地贴近大地，贴近人民，记录时代的进程，在希望与梦想中汲取力量。而新的一年，是中共十六大确定全面建设小康社会目标后的第一年。我们没有回避“全面小康”与“公正社会”这么严肃的话题，哪怕是在本该轻松的新年第一天，因为我们知道，“解决问题”与“发展”是一个概念的两面，我们正视问题，因为有一个值得冀望的明天摆在所有中国人面前。【一】这片土地正在发生着令人目眩的变化：珠三角蓬勃的加工企业，构成了全球化工业链条中不可或缺的一环；大上海重现“海上繁华梦”，离国际大都市仅一步之遥；首都北京还有5年就要举办奥运会了，正在以上千亿的投入打造一个“新北京”，迎接来自远方的客人。毫无疑问，我们身处一个空前繁荣的时代。物质的极大丰富、国力的极大增强，都表明这个国家正呈现出一种蓬勃向上的发展态势。但是，仍然有一些信息不能被我们忽视，那是上千万刚刚进入都市的民工惘然的脸庞，是都市里失业人员的艰难谋生，是都市边缘的不能称为学校的“民工子弟学校”；是上海、北京和广州等特大城市之外的更为广袤的土地，是那些土地上祖祖辈辈顽强生存着的人们……其实，江泽民在中共十六大报告中已经对当前形势作了深刻的阐述：“经过全党和全国各族人民的共同努力，我们胜利实现了现代化建设‘三步走’战略的第一步、第二步目标，人民生活总体上达到小康水平。这是社会主义制度的伟大胜利，是中华民族发展史上一个新的里程碑。”他同时指出，“现在达到的小康还是低水平的、不全面的、发展很不平衡的小康”，“城乡二元经济结构还没有改变，地区差距扩大的趋势尚未扭转，贫困人口还为数不少；……就业和社会保障压力增大……”。如果不能清醒地认清形势，盲目的乐观可能会干扰我们前进的方向。所以，我们又一次选择了行动。十多名记者以沈阳、北京、上海、江苏宜兴、广州5个地域为原点，出发。方向是既定的：向西；距离是特定的：在50里左右。方向和距离并不代表什么，恰恰表明了选择的随意。我们只想为急速变化中的中国划出5个横切面，作一次社会学意义上的观察。目的只有一个：我们力图展现一个真实的中国。【二】这是一个剧变的时代。如同有专家所说，自晚清以降，经过多少仁人志士的热血奋争，中国人一百多年来的最大命题———“从传统社会向现代社会的转变”，现已进入“总结期”。正因为如此，变化之剧烈、转型之迅猛、牵涉面之广，可想而知。如何看待正在发生的剧变，便成为这个时代的一个重大命题。我们认为，只有把这些变化放在历史的大背景之下来审视，我们才不至于在纷繁的表象面前变得迷惘而再次失去方向。那便是：一百多年来，虽然历经曲折，但中国社会从传统到现代、从封闭到开放、从封建王朝的专制统治到人民共和国的民主自由，这一“历史之河”无疑是朝着正确的方向流淌，而且不可逆转。但历史又不只是几页冰冷的纸张。只有把变化同时放在现实中、放在每个社会个体的日常悲欢中来考察，我们才能在认清方向的同时更加清醒，而不至于走向虚妄。那便是：即使在一个变动的时代，每个个体的幸福和上升通道仍应该尽可能地获得很好的制度保证。具体而言，在健康的力量正在生长和发育、社会总体走向繁荣的同时，我们更应该正视更广大社会群体的基本诉求，对生存保障、对幸福生活和美好未来、对社会公正的诉求。没有人能忽视、也没有人会忽视他们的诉求。尤其在一个变动的时代，正义往往会成为变动的代价而被轻易支付，但成为悖论的另一面是：越是变动的时代，人们也越呼唤正义，呼唤公平。自从18世纪欧洲启蒙思潮发端以来，“人人生而平等”的理想便从根本上改变了这个世界，成为影响全人类的最基本信仰。有历史学家甚至曾经精辟地对刚刚逝去的20世纪这样进行总结：整个世纪，人类都是在对“发展”和“平等”这两大主题的追逐和摇摆中度过的。所有关于社会治理的理论和思潮，都力图在这两者之间寻找适当的平衡点。在这个辞旧迎新的时刻，不论是我们记者的行动，还是众多知名专家的访谈，我们都想为这一问题寻找答案：当下的中国，是否已经到了把重心逐渐倾向“公平”的时刻？【三】事实上，十六大报告已经给出了答案。这次极为重要的会议，为21世纪头20年中国的发展规划了一个美好的蓝图，那便是：全面建设小康社会。本报记者采访的几乎所有专家，均不约而同地向记者提及了这一规划，并指出，它表明党和政府已经把让更多的公民享受到改革的成果、建设一个更为公正的社会，作为今后20年的重中之重。其实，这一政策取向早在5年前即已经开始显现。在改革步伐迅猛向前、而旧有的保障制度已经无力保障的时刻，1998年，朱镕基总理就明确提出将建立一个世界上最为庞大的社会保障体系，作为本届政府的几个最重要的目标之一。而旨在缩小东西部差距的西部大开发，也正在付诸实施。就在刚刚逝去的2002年里，从年初试图采取“国有股减持”来充实社会保障基金、从不惜排除重重阻力将农村税费改革推行到20个省，都显示出高层关怀困难群体、确保社会公正的良苦用心。这也是我们认为，2002年将成为中国社会进程中一个有标志意义的年度的重要原因。与此同时，实现社会公正的根本路径也已相当清晰地在我们面前展现开来。规则公平是最起码的公平，如果没有透明和公正的规则，权钱交易可以获取巨额利润，这对公平将是釜底抽薪。即使在规则平等的前提下，天生禀赋的差异，加上成功者对利润最大化的追逐，仍然会导致结果的巨大差异。此时，便需要政府在教育、就业、保障等公共政策上进行有效的调节，更多地倾向困难群体，以维持社会的总体和谐。这被称为“矫正的正义”。而在公共政策的选择受到各种利益集团相互博弈、相互影响的情况下，保障各个群体的政治权利便成为能够实现“矫正的正义”的当务之急。也只有在政治权利得到充分的尊重与保障的情况下，制定公正的规则这一更为重要的前提才成为可能。于是，尊重宪法、依法治国、建设社会主义民主政治和政治文明的道路便成为根本的选择。按他国的经验，在一个特定的历史阶段，往往会在这三个层面的选择中偏向一个。然而，对于剧变中的中国而言，这三个层面的选择似乎正显得同样重要而迫切。正因为如此，这才是需要决策层和全体国民的大智慧的时候。所以，我们寄望于2003年，寄望于今后20年。