<?xml version="1.0" encoding="utf-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Bug1024</title><link>https://bug1024.com/</link><description>工作中的系统与人，生活里的选择与时间，偶尔也谈技术。</description><generator>Hugo 0.161.1 https://gohugo.io/</generator><language>zh-cn</language><copyright>© 2026 Bug1024</copyright><lastBuildDate>Sat, 18 Jul 2026 18:24:39 +0000</lastBuildDate><atom:link rel="self" type="application/rss+xml" href="https://bug1024.com/rss.xml"/><item><title>
“业务优先”的边界</title><link>https://bug1024.com/posts/boundaries-of-business-first/</link><guid isPermaLink="true">https://bug1024.com/posts/boundaries-of-business-first/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;“业务优先”很少会遭到公开反对。&lt;/p&gt;
&lt;p&gt;对技术团队来说，它意味着不只关注代码，也要理解需求为什么存在，尽快解决真实问题。相比用流程和职责拒绝一切额外工作，这种态度当然更积极。&lt;/p&gt;
&lt;p&gt;但在实际协作中，业务优先也很容易被简化成另一件事：谁的诉求最急，就先帮谁做；正常流程太慢，就绕开流程；某个环节能力不足，技术团队便直接补上。&lt;/p&gt;
&lt;p&gt;这些做法短期内往往有效，甚至容易得到认可。问题通常在更晚的时候出现。&lt;/p&gt;
&lt;h2 id="局部的着急不等于整体的优先级"&gt;局部的着急不等于整体的优先级&lt;/h2&gt;
&lt;p&gt;我见过这样的情况：业务方带着一个很急的诉求找到研发，研发手上正好有时间，便提前开始实现。等产品完成调研，才发现相似诉求不只来自一个团队，原来的方案只能解决其中很小的一部分，已经完成的代码不得不重新调整。&lt;/p&gt;
&lt;p&gt;单独看这次协作，每个人都有合理的理由。业务希望尽快解决问题，研发也表现出了主动性。但双方掌握的信息都不完整，缺少的恰好是产品调研和统一决策所提供的全局视角。&lt;/p&gt;
&lt;p&gt;组织设置分工，不只是为了规定谁来写文档、谁来排期。更重要的作用，是让不同位置的人补充各自掌握的信息。绕开其中一个环节，有时确实能节省等待时间，也可能同时失去这个环节原本负责的判断。&lt;/p&gt;
&lt;p&gt;所以，主动响应不一定意味着立即开发。研发可以先确认问题、提供技术建议、评估成本，甚至做小范围验证。但在信息不足时直接承诺完整方案，通常是在用确定的投入应对尚未确定的问题。&lt;/p&gt;
&lt;h2 id="不断补位会让问题难以被看见"&gt;不断补位会让问题难以被看见&lt;/h2&gt;
&lt;p&gt;还有一种更常见的情况：某个协作环节长期运转不畅，技术团队为了保证结果，不断承担额外工作。&lt;/p&gt;
&lt;p&gt;偶尔补位是正常协作。线上出了问题，不可能等职责讨论清楚以后再止损；项目临近交付，也很难要求每一步都回到理想流程。&lt;/p&gt;
&lt;p&gt;但如果补位成为固定做法，组织看到的可能只是“事情最后完成了”。原本存在的信息缺失、流程问题和资源不足，被额外投入掩盖起来。下一次遇到类似问题，所有人仍然会按原来的方式行动，只是继续依赖少数人兜底。&lt;/p&gt;
&lt;p&gt;时间长了以后，责任边界越来越模糊。业务认为技术总有办法，技术认为其他环节没有尽责；系统中堆积了越来越多临时实现，团队则把大量时间用于救火。等业务效果没有达到预期时，复杂、缓慢而不稳定的系统又会成为最容易被看见的问题。&lt;/p&gt;
&lt;p&gt;主动补位本身没有错。需要同时做的，是记录补位为什么发生，说明它付出了什么成本，并推动导致问题的条件发生变化。否则，一次次解决表面问题，只会让根因更晚暴露。&lt;/p&gt;
&lt;h2 id="技术团队也有自己的本职工作"&gt;技术团队也有自己的本职工作&lt;/h2&gt;
&lt;p&gt;当业务需求持续到来时，架构调整、质量建设和工程效率很难表现出同等紧迫性。一个新功能延期，马上会有人追问；一次重构没有开始，短期内通常不会发生什么。&lt;/p&gt;
&lt;p&gt;因此，“业务优先”如果只按照眼前的可见收益理解，技术团队很容易把所有时间用于交付需求。代码仍然能运行，功能也在增加，但每一次修改都比上一次更慢，问题排查越来越依赖熟悉历史的人。&lt;/p&gt;
&lt;p&gt;保障稳定性、控制复杂度、改善可维护性，同样是在支持业务。区别只在于，它们的收益较晚出现，失败的代价也常常在很久以后才被计算。&lt;/p&gt;
&lt;p&gt;技术人员需要理解业务，但不应该因此放弃技术判断。真正困难的地方不是在业务与技术之间选择一边，而是说明眼前需求、长期成本和系统风险之间的关系，再共同决定承担什么代价。&lt;/p&gt;
&lt;h2 id="主动性的合适位置"&gt;主动性的合适位置&lt;/h2&gt;
&lt;p&gt;现在我更愿意把业务优先理解为一种判断顺序：先确认要解决的业务问题，再讨论技术方案和协作方式。它不是要求技术团队接受所有请求，也不是授权任何人绕开其他角色。&lt;/p&gt;
&lt;p&gt;研发的主动性可以用在很多地方：提前了解业务规划，指出方案中的技术约束，补充产品未掌握的信息，为不确定的方向做低成本验证，或者在问题发生后推动根因改进。&lt;/p&gt;
&lt;p&gt;这些事情未必像快速上线一个功能那样显眼，却更有可能减少返工和消耗。&lt;/p&gt;
&lt;p&gt;职责清楚也不意味着彼此冷漠。恰恰因为把对方当作专业的合作伙伴，才需要让信息经过必要的环节，让决定由掌握相应信息的人共同作出。&lt;/p&gt;
&lt;p&gt;业务优先没有问题。需要警惕的是，把它变成一条不再需要判断的口号。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
不要急着批评一套旧系统</title><link>https://bug1024.com/posts/dont-rush-to-criticize-legacy-systems/</link><guid isPermaLink="true">https://bug1024.com/posts/dont-rush-to-criticize-legacy-systems/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;进入一个新团队以后，我曾经很快列出过一批系统问题。&lt;/p&gt;
&lt;p&gt;接口返回不统一，配置文件分散，公共能力没有封装，代码命名也不一致。有些问题确实影响开发效率，从技术规范看也不难判断。&lt;/p&gt;
&lt;p&gt;现在回看，那份清单并非完全错误，只是下结论的速度比理解系统的速度快。&lt;/p&gt;
&lt;h2 id="新人为什么更容易看见问题"&gt;新人为什么更容易看见问题&lt;/h2&gt;
&lt;p&gt;长期维护一套系统的人已经习惯了它的结构，也知道许多约定背后的默认前提。新人没有这些习惯，第一次遇到不清楚的接口或流程时，更容易感受到额外成本。&lt;/p&gt;
&lt;p&gt;这种陌生视角很有价值。很多被团队视为理所当然的问题，只有新人会继续追问为什么。&lt;/p&gt;
&lt;p&gt;但看见不便，与理解不便怎样形成，是两件不同的事。&lt;/p&gt;
&lt;h2 id="代码里看不到全部历史"&gt;代码里看不到全部历史&lt;/h2&gt;
&lt;p&gt;一段重复逻辑可能源于当时两个团队独立交付；一个看起来过度复杂的兼容层，可能为了迁移不能升级的旧客户端；某个服务边界不合理，也许对应过往组织结构。&lt;/p&gt;
&lt;p&gt;这些理由不一定能证明现状合理，却会影响改进方式。忽略历史直接重写，可能再次遇到前人已经处理过的约束。&lt;/p&gt;
&lt;p&gt;旧系统的问题通常不是某个人一次设计出来，而是在持续交付、人员变化和局部妥协中逐渐形成。&lt;/p&gt;
&lt;h2 id="区分真实成本与个人偏好"&gt;区分真实成本与个人偏好&lt;/h2&gt;
&lt;p&gt;工程师很容易把熟悉的技术风格当成更好的答案。&lt;/p&gt;
&lt;p&gt;命名方式、框架选择和代码组织可以有多种合理方案。只是不符合自己的习惯，并不足以证明系统需要修改。&lt;/p&gt;
&lt;p&gt;更值得优先处理的问题，通常已经产生具体成本：修改容易引发故障，新人难以上手，同类需求不断重复开发，排查依赖少数人，或者发布相互影响。&lt;/p&gt;
&lt;p&gt;能够说明成本，建议才有讨论基础。&lt;/p&gt;
&lt;h2 id="提建议也需要选择时机"&gt;提建议也需要选择时机&lt;/h2&gt;
&lt;p&gt;刚入职时立即给出大量改造建议，容易让维护者觉得过去工作被简单否定。即使建议正确，也未必获得支持。&lt;/p&gt;
&lt;p&gt;更有效的方式，是先完成一次真实任务，理解团队怎样开发和上线，再围绕已经遇到的问题提出有限改进。小范围验证比完整重构方案更容易建立共识。&lt;/p&gt;
&lt;p&gt;对于稳定性风险，当然不必等到完全熟悉以后才指出。但也应该区分明确风险与基于经验的怀疑。&lt;/p&gt;
&lt;h2 id="理解不是替旧系统辩护"&gt;理解不是替旧系统辩护&lt;/h2&gt;
&lt;p&gt;强调历史条件，不代表所有技术债务都应该继续保留。&lt;/p&gt;
&lt;p&gt;有些临时方案早已失去原来的前提，有些结构确实持续消耗团队。理解它们为何存在，是为了知道从哪里改变，而不是为不改变寻找理由。&lt;/p&gt;
&lt;p&gt;现在面对陌生系统，我仍然会记录第一印象。只是不会急着把它们都定义为问题。&lt;/p&gt;
&lt;p&gt;先看系统怎样运行，哪些人依赖它，过去为什么作出这些选择，再判断什么值得改变。这个顺序通常比快速证明自己看得更清楚重要。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
成为负责人以后</title><link>https://bug1024.com/posts/after-becoming-a-lead/</link><guid isPermaLink="true">https://bug1024.com/posts/after-becoming-a-lead/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;成为小团队负责人以后，我最初并没有觉得工作发生了根本变化。&lt;/p&gt;
&lt;p&gt;需求还是那些需求，代码仍然要写，遇到问题也照样需要排查。区别似乎只是多参加几场会议，多分配一些任务，再对项目进度负责。&lt;/p&gt;
&lt;p&gt;真正的变化过了一段时间才显现出来：过去我主要确保自己把事情做好，后来则要让一群人能够稳定地把事情做好。&lt;/p&gt;
&lt;h2 id="自己会做不等于团队能做到"&gt;自己会做，不等于团队能做到&lt;/h2&gt;
&lt;p&gt;个人开发遇到问题，可以依靠经验快速处理。团队工作却不能长期依赖某个人的经验。&lt;/p&gt;
&lt;p&gt;如果一项任务只有我知道应该注意什么，我可以每次亲自检查；如果一个系统只有少数人能够排查，我也可以在故障时及时介入。短期看，这是最高效的方式。时间久了，负责人会成为所有事情的必经节点，团队成员也很难建立完整判断。&lt;/p&gt;
&lt;p&gt;因此，一些过去可以依靠默契完成的事情，需要被说清楚：目标是什么，为什么这样设计，什么情况必须同步，哪些质量标准不能因为时间紧而放弃。&lt;/p&gt;
&lt;p&gt;这并不意味着建立越多流程越好。流程只是把重复判断固定下来，让团队把精力留给真正需要判断的问题。&lt;/p&gt;
&lt;h2 id="信息需要被反复确认"&gt;信息需要被反复确认&lt;/h2&gt;
&lt;p&gt;程序的反馈通常明确。输入、输出、成功和失败都有相对清楚的定义。团队沟通则不是这样。&lt;/p&gt;
&lt;p&gt;同一句话，不同的人可能根据各自掌握的背景作出不同理解。会议上没人提出异议，也不代表所有人已经形成一致认识。有些人记住了目标，却没有理解优先级；有些人理解了方案，却不知道它受到哪些约束。&lt;/p&gt;
&lt;p&gt;负责人不能只负责“说过”，还要确认关键信息是否真正到达。重要目标需要重复，复杂问题需要写下来，分歧也要在执行之前尽量暴露。&lt;/p&gt;
&lt;p&gt;这件事看起来低效，但很多返工恰恰来自对沟通效果的高估。&lt;/p&gt;
&lt;h2 id="不再用自己的速度要求所有人"&gt;不再用自己的速度要求所有人&lt;/h2&gt;
&lt;p&gt;刚开始带团队时，很容易直接拿自己的做事方式作为标准。&lt;/p&gt;
&lt;p&gt;我知道某项任务应该从哪里开始，便会觉得别人也应该迅速找到入口；一个问题已经指出，便期待对方下一次立即改变。&lt;/p&gt;
&lt;p&gt;后来才意识到，经验无法通过一次提醒完成转移。人对问题的理解、工作习惯和风险敏感度，都需要在具体事情中逐渐形成。&lt;/p&gt;
&lt;p&gt;负责人要给出反馈，也要接受变化没有那么快。真正需要观察的，不是对方是否立刻变成理想状态，而是有没有理解问题、是否愿意修正，以及团队能否为这种修正提供条件。&lt;/p&gt;
&lt;h2 id="负责人仍然需要专业判断"&gt;负责人仍然需要专业判断&lt;/h2&gt;
&lt;p&gt;从技术岗位走向管理以后，还有一种诱惑：把所有专业工作都交出去，只保留目标、排期和汇报。&lt;/p&gt;
&lt;p&gt;但小团队负责人如果完全远离实际工作，很快会失去对成本和风险的判断。无法判断方案是否合理，也很难知道成员遇到的困难究竟来自能力、资源还是目标本身。&lt;/p&gt;
&lt;p&gt;保持专业能力不等于包办实现。更合适的状态是，关键问题能够深入，日常工作敢于交给成员，并对最终标准负责。&lt;/p&gt;
&lt;h2 id="工作对象发生了变化"&gt;工作对象发生了变化&lt;/h2&gt;
&lt;p&gt;成为负责人以后，产出不再只体现在自己写了多少代码。&lt;/p&gt;
&lt;p&gt;一项目标是否清楚，一次分歧是否及时解决，一个新人能否更快理解系统，一个重复问题有没有形成改进机制，这些都开始成为工作的一部分。&lt;/p&gt;
&lt;p&gt;它们不如完成一个功能直观，也很难全部量化。但团队能否减少无谓消耗，往往取决于这些看起来不够具体的工作。&lt;/p&gt;
&lt;p&gt;负责人不是拥有更多控制权的人，而是需要处理更多不确定性的人。这是我在角色变化之后才逐渐理解的区别。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
从 ToC 到 ToB</title><link>https://bug1024.com/posts/from-consumer-to-enterprise-software/</link><guid isPermaLink="true">https://bug1024.com/posts/from-consumer-to-enterprise-software/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;早期做面向消费者的互联网系统时，我最容易感知的技术挑战来自数据量和并发。&lt;/p&gt;
&lt;p&gt;一次活动会带来多少流量，缓存能否承受，数据库查询是否足够快，消息是否会堆积。这些问题不一定容易解决，但衡量方式相对直接：耗时、吞吐、错误率和资源使用都能提供反馈。&lt;/p&gt;
&lt;p&gt;后来参与企业软件建设，我遇到了另一种复杂度。&lt;/p&gt;
&lt;p&gt;系统的访问量没有那么极端，业务模型却明显更难保持清楚。&lt;/p&gt;
&lt;h2 id="更多角色意味着更多视角"&gt;更多角色意味着更多视角&lt;/h2&gt;
&lt;p&gt;面向普通消费者的产品，用户身份通常相对统一。企业软件则可能同时服务公司管理者、项目负责人、一线操作人员、财务和外部合作方。&lt;/p&gt;
&lt;p&gt;他们面对的是同一批业务数据，能够执行的操作和关心的信息却完全不同。一个状态对操作人员意味着下一步任务，对管理者可能只是统计口径，对财务又关系到能否结算。&lt;/p&gt;
&lt;p&gt;权限因此不只是判断“能不能访问”。它会与组织层级、数据范围、业务阶段和具体角色结合。组织调整以后，原有权限模型也可能随之变化。&lt;/p&gt;
&lt;p&gt;当角色没有定义清楚时，代码里便会出现越来越多针对身份和场景的条件判断。&lt;/p&gt;
&lt;h2 id="流程并不总是标准的"&gt;流程并不总是标准的&lt;/h2&gt;
&lt;p&gt;企业之间的工作方式存在差异。即使处理相似业务，不同公司也可能使用不同字段、审核节点和责任划分。&lt;/p&gt;
&lt;p&gt;产品希望建立标准流程，客户则希望系统适应已经运行多年的习惯。两种诉求都合理，系统只能在标准化与定制化之间选择位置。&lt;/p&gt;
&lt;p&gt;流程配置、动态表单和规则引擎能够提供灵活性，也会让一次操作的实际路径取决于当时配置。问题排查不再只是重放请求，还要还原用户身份、组织关系、业务状态和配置版本。&lt;/p&gt;
&lt;h2 id="低流量不等于低风险"&gt;低流量不等于低风险&lt;/h2&gt;
&lt;p&gt;ToB 系统的用户数量可能有限，单次操作的重要性却更高。&lt;/p&gt;
&lt;p&gt;一笔数据可能参与后续对账和结算，一个错误状态可能阻断整条业务流程。很多操作还跨越较长时间，今天录入的信息要在数周以后继续使用。&lt;/p&gt;
&lt;p&gt;这类系统对数据正确性、状态完整性和变更可追踪性的要求，并不比高并发系统低。只用接口耗时和机器负载评价稳定性，很容易忽略业务上的失败。&lt;/p&gt;
&lt;h2 id="技术需要先建立业务模型"&gt;技术需要先建立业务模型&lt;/h2&gt;
&lt;p&gt;做 ToC 系统时，我习惯先关注容量和性能。转向 ToB 以后，越来越多时间用在概念和边界上。&lt;/p&gt;
&lt;p&gt;一个业务对象在不同阶段叫什么，哪些变化可以撤销，谁有权触发，外部系统失败后本系统处于什么状态。这些问题如果没有在设计阶段说明，最终都会以代码分支的形式出现。&lt;/p&gt;
&lt;p&gt;业务抽象不是把现实压缩成几个漂亮的类，而是尽可能找到在变化中仍然稳定的关系。哪些属于核心模型，哪些只是展示差异，哪些必须保留为客户配置，需要产品与技术共同判断。&lt;/p&gt;
&lt;h2 id="两种复杂度并没有高下之分"&gt;两种复杂度并没有高下之分&lt;/h2&gt;
&lt;p&gt;ToC 和 ToB 并不是两套完全对立的技术。企业系统同样会遇到性能问题，消费者产品也有复杂流程。&lt;/p&gt;
&lt;p&gt;真正的变化是，过去我容易把系统复杂度等同于流量规模，后来才意识到，复杂度也可能来自角色数量、规则差异、时间跨度和组织边界。&lt;/p&gt;
&lt;p&gt;看见复杂度来自哪里，才知道应该把设计精力放在哪里。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
从完成任务到承担结果</title><link>https://bug1024.com/posts/from-task-to-outcome/</link><guid isPermaLink="true">https://bug1024.com/posts/from-task-to-outcome/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;刚工作时，我判断一项任务是否完成，标准相对简单：功能实现了，测试通过了，按计划上线了。&lt;/p&gt;
&lt;p&gt;这是研发工作最基本的要求，也是一套清楚的评价方式。需求由产品提出，研发负责实现，测试确认质量。每个人把自己负责的部分做好，项目便能向前推进。&lt;/p&gt;
&lt;p&gt;后来负责的项目变得复杂，我逐渐发现，“我的部分已经完成”并不总能换来一个有效结果。&lt;/p&gt;
&lt;h2 id="做完不等于问题解决"&gt;做完不等于问题解决&lt;/h2&gt;
&lt;p&gt;有些功能按需求实现以后，使用情况并不理想。也有一些项目顺利上线，过了一段时间却发现维护成本远高于预期。&lt;/p&gt;
&lt;p&gt;从研发视角看，代码没有明显问题。回到业务目标，情况却没有那么乐观：有时需求只覆盖了局部场景，有时上下游对规则的理解并不一致，有时用户真正遇到的阻力根本不在系统功能上。&lt;/p&gt;
&lt;p&gt;这些问题很难仅靠提高编码质量解决。它要求研发在开发之前多问一步：为什么要做，当前方案准备改变什么，怎样判断改变确实发生了。&lt;/p&gt;
&lt;p&gt;这并不意味着每个工程师都要代替产品做决策。研发掌握的是另一部分信息——系统现状、实现成本、数据限制和长期风险。把这些信息带入讨论，本身就是对结果负责的一部分。&lt;/p&gt;
&lt;h2 id="责任范围开始向前和向后延伸"&gt;责任范围开始向前和向后延伸&lt;/h2&gt;
&lt;p&gt;过去我更关注开发阶段，后来注意力逐渐向两端延伸。&lt;/p&gt;
&lt;p&gt;向前，是在方案确定之前理解需求和约束。如果一个概念没有说清楚，或者多个角色对状态流转理解不同，越早暴露，修正成本越低。&lt;/p&gt;
&lt;p&gt;向后，则是关注上线以后发生了什么。监控是否覆盖关键环节，用户有没有真正使用，原先预估的数据量是否准确，临时兼容逻辑何时退出。这些信息会反过来验证最初的判断。&lt;/p&gt;
&lt;p&gt;任务由此不再是需求系统里的一张卡片，而是一段从问题提出到结果被观察的过程。&lt;/p&gt;
&lt;h2 id="承担结果不是无限兜底"&gt;承担结果不是无限兜底&lt;/h2&gt;
&lt;p&gt;“Owner 意识”有时会被理解为，凡是与结果相关的事情都应该主动接过来。这样做短期内很有效，却很容易让责任无限扩张。&lt;/p&gt;
&lt;p&gt;一个人不可能同时掌握产品、业务、技术和运营的全部信息。不断替其他环节完成工作，也可能让真实问题无法被组织看见。&lt;/p&gt;
&lt;p&gt;我现在更愿意把承担结果理解为三件事：不因为不在自己职责内就忽略已经看见的风险；把问题同步给真正能够作出决定的人；在自己的专业范围内推动问题继续向前。&lt;/p&gt;
&lt;p&gt;必要时可以补位，但补位之后还要恢复正常的协作方式。否则，主动性会逐渐变成对个人的依赖。&lt;/p&gt;
&lt;h2 id="结果也不完全由个人控制"&gt;结果也不完全由个人控制&lt;/h2&gt;
&lt;p&gt;关注结果之后，另一个容易出现的误区，是把所有结果都归因于个人能力。&lt;/p&gt;
&lt;p&gt;项目成功可能来自市场机会、团队协作和平台条件；项目没有达到目标，也可能受资源、时间与外部环境影响。负责人需要承担自己能够改变的部分，但不必假装一切都在控制之中。&lt;/p&gt;
&lt;p&gt;更准确的复盘应该区分：哪些判断由自己作出，哪些条件当时无法改变，哪些风险已经被接受，以及下一次能够提前做什么。&lt;/p&gt;
&lt;p&gt;这些区分不会削弱责任，反而让责任变得具体。&lt;/p&gt;
&lt;h2 id="工作标准的变化"&gt;工作标准的变化&lt;/h2&gt;
&lt;p&gt;完成任务仍然重要。没有稳定的执行，谈结果只会变成空泛表态。&lt;/p&gt;
&lt;p&gt;变化在于，执行不再是工作的终点。我需要知道自己完成的部分位于哪条链路，影响了谁，又留下了什么成本。&lt;/p&gt;
&lt;p&gt;从完成任务到承担结果，并不是角色突然升级，而是判断工作是否结束的标准发生了变化。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
代码评审不能依赖一个人</title><link>https://bug1024.com/posts/code-review-cannot-depend-on-one-person/</link><guid isPermaLink="true">https://bug1024.com/posts/code-review-cannot-depend-on-one-person/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我曾经在一段时间里评审团队的大部分代码。&lt;/p&gt;
&lt;p&gt;项目刚开始建设，成员对业务、框架和代码标准的理解还不一致。由相对熟悉全局的人集中评审，能够尽快发现问题，也能避免不同模块在早期形成完全不同的实现方式。&lt;/p&gt;
&lt;p&gt;这种方式在初期很有效，时间长了却会产生另一个问题：几乎所有代码都在等待同一个人。&lt;/p&gt;
&lt;h2 id="集中评审为什么有效"&gt;集中评审为什么有效&lt;/h2&gt;
&lt;p&gt;新团队需要建立共同尺度。&lt;/p&gt;
&lt;p&gt;哪些业务对象应该复用，异常怎样处理，接口如何兼容，什么逻辑应该放在公共层，这些问题无法只靠格式规范解决。集中评审让同一个判断能够较快覆盖多个模块，也便于发现项目之间的重复实现。&lt;/p&gt;
&lt;p&gt;对负责人来说，Review 还是理解系统变化的一种方式。即使没有亲自参与每项开发，也能持续知道模型、依赖和风险发生了什么变化。&lt;/p&gt;
&lt;p&gt;因此，集中评审本身并不是问题。问题在于它是否只是一个阶段。&lt;/p&gt;
&lt;h2 id="负责人逐渐成为瓶颈"&gt;负责人逐渐成为瓶颈&lt;/h2&gt;
&lt;p&gt;当提交数量增加，评审会不断挤占其他工作。开发完成以后等待 Review，紧急需求又会压缩评审时间。为了不影响进度，负责人容易快速扫过代码，只检查明显错误。&lt;/p&gt;
&lt;p&gt;这时集中评审已经失去最初作用。它仍然保留一道流程，却无法提供足够深入的判断。&lt;/p&gt;
&lt;p&gt;更隐蔽的影响是，团队成员会逐渐把代码质量理解为评审者的责任。只要最后有人把关，提交之前就不必完成同等强度的自查。负责人越认真，团队反而越可能依赖这个人。&lt;/p&gt;
&lt;h2 id="review-应该看什么"&gt;Review 应该看什么&lt;/h2&gt;
&lt;p&gt;代码风格、空指针和常见错误可以借助静态检查。人更应该关注工具难以判断的部分：实现是否符合业务含义，边界是否清楚，失败路径有没有处理，修改是否破坏已有约定，以及未来的人能否理解这段代码为何存在。&lt;/p&gt;
&lt;p&gt;这要求评审者了解上下文。不了解需求的人可以发现局部代码问题，却很难判断模型是否正确。因此，不能简单把所有提交随机分配给任何成员。&lt;/p&gt;
&lt;p&gt;更合适的方式，是让熟悉相邻模块的人交叉评审，关键设计由负责人把关，常规实现则逐步交给团队共同承担。&lt;/p&gt;
&lt;h2 id="从指出问题到形成标准"&gt;从指出问题到形成标准&lt;/h2&gt;
&lt;p&gt;同一个问题反复出现在 Review 中，说明只靠评论没有形成改变。&lt;/p&gt;
&lt;p&gt;可以自动检查的，应进入工具；属于共同约定的，应写进简短规范或示例；涉及设计能力的，则需要在评审中解释原因，而不是只留下“请修改”。&lt;/p&gt;
&lt;p&gt;一次评论解决一段代码，解释判断依据才可能影响下一次实现。&lt;/p&gt;
&lt;h2 id="负责人如何退出中心"&gt;负责人如何退出中心&lt;/h2&gt;
&lt;p&gt;让更多成员参与评审，不等于立即平均分配责任。&lt;/p&gt;
&lt;p&gt;可以先从熟悉模块开始，让提交者说明改动背景和风险，再由其他成员给出意见。负责人继续观察关键变更，但不再成为每次合并的唯一前置条件。&lt;/p&gt;
&lt;p&gt;这个过程会暂时增加沟通成本，也可能出现判断不一致。只有经过这些讨论，团队才会真正建立共同尺度。&lt;/p&gt;
&lt;p&gt;代码评审的目标不是保证所有问题都由某个人发现。更稳定的结果，是让代码质量不再依赖某个人一直在场。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
第一次实习时，我都记了些什么</title><link>https://bug1024.com/posts/first-internship-notes/</link><guid isPermaLink="true">https://bug1024.com/posts/first-internship-notes/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;2014年第一次实习时，我留下了一份很长的学习笔记。&lt;/p&gt;
&lt;p&gt;里面有 Linux 命令、Nginx 配置、PHP 框架用法，也有版本管理、项目流程和数据库操作。许多内容只是从网页上复制下来的说明，有些命令今天已经不会再用。&lt;/p&gt;
&lt;p&gt;当时并不知道什么最重要，所以几乎把遇到的一切都记了下来。&lt;/p&gt;
&lt;h2 id="先让电脑能够工作"&gt;先让电脑能够工作&lt;/h2&gt;
&lt;p&gt;实习开始后的第一批任务，不是设计功能，而是安装系统、配置开发环境、拉取代码和启动项目。&lt;/p&gt;
&lt;p&gt;这些事情在学校里很少被单独讨论。写程序时，环境通常已经准备好；到了公司，才知道代码能够运行依赖许多前置条件：软件版本、配置文件、数据库、域名解析和文件权限，任何一处不一致都可能让项目无法启动。&lt;/p&gt;
&lt;p&gt;我在笔记里详细记录每一步操作。现在看起来有些机械，但它提供了一种确定感。第一次进入陌生环境，能够按照自己的记录再次把服务启动起来，就意味着开始具备独立工作的可能。&lt;/p&gt;
&lt;h2 id="会写代码只是其中一部分"&gt;会写代码只是其中一部分&lt;/h2&gt;
&lt;p&gt;真正参与开发以后，我接触到需求、任务拆分、测试和上线。一个功能不再是写完能够运行就算结束，还需要符合团队规范，与已有代码协作，通过测试，并按约定流程发布。&lt;/p&gt;
&lt;p&gt;我也是在那时第一次具体理解“软件工程”与个人编程的区别。&lt;/p&gt;
&lt;p&gt;学校作业的边界通常很清楚，程序主要由自己负责。公司里的一个小改动，却可能依赖前端、数据库和其他接口，也可能影响已经在线使用的功能。代码质量不只是写得是否漂亮，还包括别人能否理解、出错后是否容易定位，以及修改时会不会破坏原有行为。&lt;/p&gt;
&lt;h2 id="什么都记是因为还没有判断标准"&gt;什么都记，是因为还没有判断标准&lt;/h2&gt;
&lt;p&gt;早期笔记里既有重要原理，也有随手就能查到的语法。它们没有清楚层级，基本按照遇到的顺序排列。&lt;/p&gt;
&lt;p&gt;这不是一种高效的知识结构，却是初学阶段的真实状态。只有接触过足够多的问题，才逐渐知道哪些属于基础规律，哪些只是工具细节，哪些需要长期记住，哪些临时查询即可。&lt;/p&gt;
&lt;p&gt;后来我的笔记开始按主题整理，也越来越少记录具体命令。不是记忆力变好了，而是判断发生了变化：能够知道一个问题大致位于哪里，需要时再找到资料。&lt;/p&gt;
&lt;h2 id="第一份工作教会我的事"&gt;第一份工作教会我的事&lt;/h2&gt;
&lt;p&gt;那段实习没有留下什么值得公开的复杂项目。更重要的变化是，我第一次知道职业开发者每天具体在做什么。&lt;/p&gt;
&lt;p&gt;工作并不是不断写出新的程序。更多时候是在理解已有系统，按照团队方式完成有限改动，再确认改动没有带来额外问题。&lt;/p&gt;
&lt;p&gt;这听起来并不特别，却构成了后来很多工作的基础。&lt;/p&gt;
&lt;p&gt;如今再看那份笔记，我不会把其中的大部分技术内容迁入博客。它真正值得保留的，不是十年前某个框架的用法，而是一个刚进入行业的人，怎样从不会工作开始，逐渐找到自己的位置。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
第一次遇到 JVM Crash</title><link>https://bug1024.com/posts/first-jvm-crash/</link><guid isPermaLink="true">https://bug1024.com/posts/first-jvm-crash/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我第一次遇到 JVM Crash，是在一个业务刚开始扩大使用范围的时候。&lt;/p&gt;
&lt;p&gt;服务会在高峰期出现大量 5xx，随后进程自动重启。奇怪的是，崩溃之前没有明显的 GC 异常，CPU 和内存也没有出现持续抖动。日志里偶尔出现一些难以关联的空指针，但无法稳定复现。&lt;/p&gt;
&lt;p&gt;普通 Java 异常通常只会影响一次请求。JVM 进程直接退出，意味着问题可能已经越过应用代码边界。&lt;/p&gt;
&lt;h2 id="先让服务能够继续运行"&gt;先让服务能够继续运行&lt;/h2&gt;
&lt;p&gt;排查根因之前，首先要降低业务影响。&lt;/p&gt;
&lt;p&gt;我们增加了服务资源，并通过平滑重启减少进程退出带来的中断。这些措施没有解决问题，但让服务在崩溃发生时能够更快恢复，也为后续排查争取了时间。&lt;/p&gt;
&lt;p&gt;止损和根因分析需要分开。线上问题中，最先实施的方案不必最接近根因，只要它能以可控代价缩小影响。&lt;/p&gt;
&lt;h2 id="几个没有奏效的方向"&gt;几个没有奏效的方向&lt;/h2&gt;
&lt;p&gt;最初的注意力集中在 JVM 本身。&lt;/p&gt;
&lt;p&gt;我们检查了 GC、线程和资源使用，也尝试调整编译相关参数。关闭分层编译以后，崩溃频率有所变化，却没有消失。&lt;/p&gt;
&lt;p&gt;随后又咨询了有相关经验的同事，陆续尝试了一些 JVM 参数。因为现象像虚拟机问题，我们还升级到当时较新的 JDK 版本，并向官方提交了错误报告。&lt;/p&gt;
&lt;p&gt;这些动作都不能算毫无依据，但结果说明它们没有触及真正原因。&lt;/p&gt;
&lt;p&gt;排查时间变长以后，很容易因为已经投入大量精力而继续沿着原方向深入。实际上，每次调整都应该被当成一次假设验证：现象是否发生变化，变化能否稳定复现，它支持还是削弱原来的判断。&lt;/p&gt;
&lt;h2 id="崩溃文件里的异常线索"&gt;崩溃文件里的异常线索&lt;/h2&gt;
&lt;p&gt;转折来自一次保留下来的 crash 文件。&lt;/p&gt;
&lt;p&gt;其中出现了一个 &lt;code&gt;AbstractMethodError&lt;/code&gt;。这个错误通常意味着运行时看到的类定义与调用方预期不一致。沿着相关对象继续追查，我们把目标从 JVM 调优转向了对象的生成方式。&lt;/p&gt;
&lt;p&gt;这个对象并不是普通代码直接创建，而是由一个序列化组件反序列化得到。组件在特定情况下生成了不一致的类结构，最终触发运行时异常，并进一步导致 JVM 崩溃。&lt;/p&gt;
&lt;p&gt;替换序列化方式以后，服务不再出现同类问题。后来我们也把现象反馈给了相关开源项目，并用同样的线索帮助其他团队处理过类似故障。&lt;/p&gt;
&lt;h2 id="为什么前面的判断会偏离"&gt;为什么前面的判断会偏离&lt;/h2&gt;
&lt;p&gt;JVM Crash 很容易让人把注意力全部放在虚拟机。&lt;/p&gt;
&lt;p&gt;但进程崩溃只是一种结果，根因可能来自 JDK、JNI、Unsafe 操作、字节码生成或第三方序列化组件。应用使用的库同样可能进入虚拟机边界以内。&lt;/p&gt;
&lt;p&gt;当时一些调参带来了频率变化，也进一步强化了“这是 JVM 编译问题”的判断。但相关性并不足以证明因果。参数改变了代码编译和执行路径，可能只是让问题更难触发，而不是消除了原因。&lt;/p&gt;
&lt;h2 id="这次排查留下的经验"&gt;这次排查留下的经验&lt;/h2&gt;
&lt;p&gt;遇到罕见故障时，广泛尝试并不完全可避免。关键是保留每次尝试的条件和结果，避免让临时变化变成结论。&lt;/p&gt;
&lt;p&gt;另一方面，崩溃文件、线程转储和系统日志即使难以理解，也比根据现象猜测更接近事实。真正的突破往往来自一个能够缩小范围的异常，而不是更多参数组合。&lt;/p&gt;
&lt;p&gt;这次问题让我第一次明确感受到：排查深度不取决于知道多少 JVM 名词，而在于能否不断修正假设，直到现象、证据和最终改动能够互相解释。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/java/">Java</category></item><item><title>
定制化的代价</title><link>https://bug1024.com/posts/cost-of-customization/</link><guid isPermaLink="true">https://bug1024.com/posts/cost-of-customization/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我曾经参与过一套企业系统的定制化改造。&lt;/p&gt;
&lt;p&gt;系统原本提供一套相对标准的业务流程。随着客户规模扩大，一些重要客户提出了自己的字段、审核节点和操作规则。需求都能找到现实理由：不同公司的组织方式不同，已经运行多年的业务也不可能完全按照系统重新设计。&lt;/p&gt;
&lt;p&gt;问题是，当这些差异进入一套标准产品以后，增加的并不只是几个字段。&lt;/p&gt;
&lt;h2 id="一个字段背后的变化"&gt;一个字段背后的变化&lt;/h2&gt;
&lt;p&gt;表面上看，定制字段是一个常见能力：后台配置字段名称和类型，前端动态展示，用户填写后由后端保存。&lt;/p&gt;
&lt;p&gt;真正实现时，问题会迅速增多。字段属于公司还是具体项目，在哪个业务阶段出现，谁可以填写，谁能够查看，是否参与审核，历史数据如何兼容，移动端和网页端是否使用同一种组件协议。&lt;/p&gt;
&lt;p&gt;当字段之间还存在联动关系时，单纯配置已经很难表达全部规则。一项选择可能决定另一项是否展示，也可能改变后续流程。配置越灵活，解释和验证配置所需的逻辑就越复杂。&lt;/p&gt;
&lt;p&gt;最后，原本清楚的主流程变成了“标准逻辑加配置解释”。系统仍然能完成相同的业务动作，但每次运行的实际路径开始依赖客户、项目、角色和当时生效的配置。&lt;/p&gt;
&lt;h2 id="开发成本只是最容易看见的部分"&gt;开发成本只是最容易看见的部分&lt;/h2&gt;
&lt;p&gt;项目排期时，我们通常先估算研发和测试需要多少时间。定制化真正长期的成本，却出现在上线以后。&lt;/p&gt;
&lt;p&gt;同一个问题在不同客户那里未必能够复现；排查时除了查看代码和数据，还要确认配置版本。新增一个标准能力，也必须验证它与已有定制规则是否冲突。文档如果没有同步更新，后来接手的人甚至很难知道某段分支为何存在。&lt;/p&gt;
&lt;p&gt;这类成本不会在需求上线时一次结清，而是分散在之后每次修改、测试和故障处理中。&lt;/p&gt;
&lt;p&gt;因此，判断一个定制需求是否值得做，不能只比较本次开发成本与客户收益。还要估计它是否改变核心模型，会不会产生长期分支，以及未来由谁维护这些差异。&lt;/p&gt;
&lt;h2 id="配置化不是自动消除复杂度"&gt;配置化不是自动消除复杂度&lt;/h2&gt;
&lt;p&gt;面对大量差异，常见方案是把规则配置化。配置确实能减少重复开发，也让部分变化不必重新发布系统。&lt;/p&gt;
&lt;p&gt;但配置只是把复杂度从代码转移到另一套表达方式。它仍然需要明确的模型、校验、版本、权限、发布和回滚。缺少这些基础能力时，配置中心很容易变成另一种难以理解的程序。&lt;/p&gt;
&lt;p&gt;我在那次项目中逐渐认识到，不是所有差异都应该抽象成无限灵活的通用能力。有些高频且稳定的变化适合配置化；有些规则虽然看起来相似，实际业务含义却完全不同，勉强统一只会让模型更加模糊。&lt;/p&gt;
&lt;h2 id="标准化也不是拒绝客户"&gt;标准化也不是拒绝客户&lt;/h2&gt;
&lt;p&gt;反过来，只强调标准产品，也可能忽略客户真实的工作方式。&lt;/p&gt;
&lt;p&gt;企业软件并不运行在真空里。客户已经存在组织、流程和数据，完全要求对方适应产品，商业上未必可行。尤其在产品发展早期，一些定制需求可能帮助团队看见原有模型没有覆盖的场景。&lt;/p&gt;
&lt;p&gt;需要区分的是：它代表一类可以进入产品的问题，还是只属于单个客户的特殊做法；它值得成为长期能力，还是只应作为有明确退出条件的临时方案。&lt;/p&gt;
&lt;h2 id="代价应当被共同看见"&gt;代价应当被共同看见&lt;/h2&gt;
&lt;p&gt;定制化没有简单的是非答案。真正的问题，是决定时是否把代价说清楚。&lt;/p&gt;
&lt;p&gt;客户收益、商业关系、交付时间、系统复杂度和后续维护都应进入同一次讨论。技术团队不能只用“架构不优雅”拒绝业务，也不应该把未来成本隐藏在一句“可以实现”背后。&lt;/p&gt;
&lt;p&gt;一次定制最终是否值得，取决于组织愿意用多大的长期复杂度，换取当下的业务价值。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
工作进入低迷期以后</title><link>https://bug1024.com/posts/when-work-feels-stagnant/</link><guid isPermaLink="true">https://bug1024.com/posts/when-work-feels-stagnant/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;工作中的低迷不一定伴随明显失败。&lt;/p&gt;
&lt;p&gt;任务仍然能够完成，收入和生活也没有突然变化，只是每天做的事情越来越像重复，过去有效的目标不再提供同样的推动力。&lt;/p&gt;
&lt;p&gt;这种状态很难描述。因为没有具体问题需要立即解决，也就很容易被归结为不够自律，或者再制定一套计划。&lt;/p&gt;
&lt;h2 id="工作很少有真正的终点"&gt;工作很少有真正的终点&lt;/h2&gt;
&lt;p&gt;在学校里，很多阶段由外部环境划定。考试结束、学期结束、毕业，都能提供清楚的完成感。同一批人也在相近时间经历相似节点。&lt;/p&gt;
&lt;p&gt;进入职场以后，这些统一节奏逐渐消失。项目上线之后还有下一次迭代，掌握一项技术以后很快又有新的工具。即便升职和加薪，也只是短暂节点，不会回答接下来应该怎样工作。&lt;/p&gt;
&lt;p&gt;当外部终点减少，个人需要自己判断一个阶段是否完成。但这个判断往往比设定任务困难。&lt;/p&gt;
&lt;h2 id="过去的目标可能已经失效"&gt;过去的目标可能已经失效&lt;/h2&gt;
&lt;p&gt;职业早期的目标通常具体：学会开发，能够独立负责项目，提高收入，进入更好的平台。&lt;/p&gt;
&lt;p&gt;这些目标一旦部分实现，继续沿用原来的评价方式，边际作用会越来越低。多学一个框架、多完成几个需求，仍然有价值，却未必再带来明显成长感。&lt;/p&gt;
&lt;p&gt;低迷有时不是缺少动力，而是旧目标已经无法解释当前阶段。人仍在按照熟悉方式努力，却不知道这些努力准备带到哪里。&lt;/p&gt;
&lt;h2 id="不急着把状态变成问题"&gt;不急着把状态变成问题&lt;/h2&gt;
&lt;p&gt;我过去遇到停滞感时，习惯迅速寻找办法：早起、学习、锻炼，或者制定新的职业计划。&lt;/p&gt;
&lt;p&gt;行动能够恢复秩序，也可能过早遮住真正的问题。如果只是不够休息，再增加任务只会加重消耗；如果工作内容已经失去挑战，单纯提高效率也不会改变体验。&lt;/p&gt;
&lt;p&gt;因此，低迷期首先需要区分几件事：是身体疲惫，还是长期缺少反馈；是能力遇到瓶颈，还是目标本身不再重要；是环境限制，还是自己没有采取行动。&lt;/p&gt;
&lt;p&gt;这些原因可能同时存在，未必能迅速得到结论。&lt;/p&gt;
&lt;h2 id="建立小一些的阶段"&gt;建立小一些的阶段&lt;/h2&gt;
&lt;p&gt;当长期方向不清楚时，我更倾向先建立有限阶段，而不是寻找新的宏大目标。&lt;/p&gt;
&lt;p&gt;选择一个真实问题，给它明确时间；完成一次项目复盘；恢复某项中断很久的习惯；或者暂时停止一项持续消耗却没有进展的计划。&lt;/p&gt;
&lt;p&gt;这些事情不能直接解决职业方向，但能重新建立行动与反馈之间的联系。很多时候，人不是需要更多鼓励，而是需要看见自己的行动确实对现实产生了作用。&lt;/p&gt;
&lt;h2 id="低迷也在提供信息"&gt;低迷也在提供信息&lt;/h2&gt;
&lt;p&gt;一段工作不再带来满足，不必立即得出“应该离开”或“必须坚持”的结论。&lt;/p&gt;
&lt;p&gt;它至少说明，过去支撑自己的某些条件发生了变化。也许需要更复杂的问题，也许需要减少无谓消耗，或者把注意力从个人能力转向更大的工作对象。&lt;/p&gt;
&lt;p&gt;我现在仍然没有一套处理低迷期的固定办法。能够做的，是不急着用忙碌覆盖它，也不把暂时缺少方向理解为个人失败。&lt;/p&gt;
&lt;p&gt;有些阶段的作用，就是让旧的目标慢慢失效，再为下一步留下重新判断的空间。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category><category domain="https://bug1024.com/tags/%E7%94%9F%E6%B4%BB%E4%B8%8E%E6%97%B6%E9%97%B4/">生活与时间</category></item><item><title>
机器资源没有耗尽，系统为什么处理不过来</title><link>https://bug1024.com/posts/bottleneck-with-idle-resources/</link><guid isPermaLink="true">https://bug1024.com/posts/bottleneck-with-idle-resources/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;一套监控系统接入的项目越来越多以后，我们发现服务端开始丢失数据。&lt;/p&gt;
&lt;p&gt;初步判断并不复杂：上游持续写入，服务端消费能力没有跟上。通常遇到这种情况，最直接的方案是增加机器或提高配置。&lt;/p&gt;
&lt;p&gt;但当时的监控显示，机器 CPU 和内存都没有达到瓶颈。系统处理不过来，资源却没有被充分使用。&lt;/p&gt;
&lt;h2 id="资源指标没有解释全部问题"&gt;资源指标没有解释全部问题&lt;/h2&gt;
&lt;p&gt;CPU 使用率不高，并不意味着程序还有等比例的处理能力。&lt;/p&gt;
&lt;p&gt;一次处理可能受到单线程、锁竞争、队列、网络连接或固定任务模型限制。只要关键路径无法并行，增加更多空闲核心也不会自动提高吞吐。&lt;/p&gt;
&lt;p&gt;我们的数据处理流程中，项目指标会进入一个分析节点。随着数据增长，单个分析节点逐渐成为串行瓶颈。其他资源仍有余量，但关键处理路径已经无法继续扩展。&lt;/p&gt;
&lt;p&gt;这也解释了为什么直接增加相同机器不一定有效：如果数据仍然落到同一个分析节点，新增资源没有进入受限路径。&lt;/p&gt;
&lt;h2 id="从单分析节点变成分析器组"&gt;从单分析节点变成分析器组&lt;/h2&gt;
&lt;p&gt;最终的改造思路，是把单个分析节点扩展为一组可配置的分析器。&lt;/p&gt;
&lt;p&gt;项目按照稳定规则选择其中一个分析器，同一项目的数据仍然进入固定节点，不同项目则能够分散处理。分析器数量可以根据总量调整，消费能力不再受单节点限制。&lt;/p&gt;
&lt;p&gt;改造以后，单位时间处理能力提高了约一半，数据丢失问题也得到控制。后续开源版本采用了相近的扩展方式，侧面验证了这个方向。&lt;/p&gt;
&lt;h2 id="为什么没有一开始这样设计"&gt;为什么没有一开始这样设计&lt;/h2&gt;
&lt;p&gt;回头看，水平扩展是很自然的答案。但系统早期数据量有限，单节点结构更简单，开发和运维成本也更低。&lt;/p&gt;
&lt;p&gt;为所有可能增长提前设计复杂扩展能力，并不一定合理。真正需要做的是知道单节点方案的上限，并在数据接近边界时能够观察到。&lt;/p&gt;
&lt;p&gt;这次问题的不足，不在于最初选择单节点，而在于容量增长以后，对关键路径的处理上限缺少提前验证。&lt;/p&gt;
&lt;h2 id="性能问题先找受限路径"&gt;性能问题先找受限路径&lt;/h2&gt;
&lt;p&gt;资源监控仍然重要，但它只能说明一部分现象。&lt;/p&gt;
&lt;p&gt;遇到吞吐不足时，应该继续确认请求在哪个阶段排队，哪些任务能够并行，哪些状态要求串行，以及增加资源是否真正进入瓶颈路径。&lt;/p&gt;
&lt;p&gt;扩容是一种手段，不是诊断结论。机器是否繁忙，与系统是否还能处理更多数据，并不总是一回事。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
几次述职之后</title><link>https://bug1024.com/posts/after-several-work-reviews/</link><guid isPermaLink="true">https://bug1024.com/posts/after-several-work-reviews/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我保存着几份2019年至2022年的述职材料。&lt;/p&gt;
&lt;p&gt;当时为了说明一年做过的工作，我会统计项目数量、代码规模、系统性能、故障发现率，也会准备架构图和趋势图。材料写得很满，几乎每一页都在证明自己做了事情。&lt;/p&gt;
&lt;p&gt;几年以后再看，数字和项目名称已经不再重要。更值得保留的，是我在几次述职之间如何改变了对工作价值的理解。&lt;/p&gt;
&lt;h2 id="最初想证明自己做得足够多"&gt;最初想证明自己做得足够多&lt;/h2&gt;
&lt;p&gt;早期写述职，我最关心的是完整。负责过的项目不能漏，解决过的问题要全部写上，技术难点最好讲得足够深入。&lt;/p&gt;
&lt;p&gt;这种思路并不奇怪。研发的很多工作不容易被直接看见。一个系统平稳运行，一次迁移没有影响业务，一项风险在上线前被处理，都不像新增功能那样有清楚的展示界面。如果自己不说，别人未必知道其中投入了什么。&lt;/p&gt;
&lt;p&gt;所以我会列出处理过多少数据、优化了多少性能、接入了多少系统。数字能够说明规模，也让我觉得评价更加客观。&lt;/p&gt;
&lt;p&gt;但数字多了以后，另一个问题出现了：听的人知道工作很复杂，却不一定知道这些复杂工作最终改变了什么。&lt;/p&gt;
&lt;p&gt;完成很多事情和产生明确结果并不是同一句话。&lt;/p&gt;
&lt;h2 id="从技术难度转向问题本身"&gt;从技术难度转向问题本身&lt;/h2&gt;
&lt;p&gt;有一次准备晋升答辩，我花了很大篇幅说明监控系统的架构升级。存储怎样迁移，处理能力如何扩展，报警引擎如何改造，每一部分都有具体技术难点。&lt;/p&gt;
&lt;p&gt;这些内容能够证明技术能力，却不能独立说明为什么值得做。只有回到最初的问题——数据为什么丢失，业务为什么难以及时发现异常，旧系统为什么无法继续支撑新的使用方式——方案的价值才变得完整。&lt;/p&gt;
&lt;p&gt;后来再写项目，我会尽量按照问题、选择和结果组织内容：当时遇到什么限制，有哪些可选方案，为什么承担某种成本，完成以后哪些指标或工作方式发生了变化。&lt;/p&gt;
&lt;p&gt;这不是汇报技巧上的变化，而是工作视角的变化。如果平时只关注自己完成的技术任务，到述职时很难突然说清它与整体目标的关系。只有在项目开始和推进过程中就持续确认目标，最后才有可说明的结果。&lt;/p&gt;
&lt;h2 id="有些结果无法由一个人独占"&gt;有些结果无法由一个人独占&lt;/h2&gt;
&lt;p&gt;述职天然以个人为主语，但大部分项目都是多人协作的结果。&lt;/p&gt;
&lt;p&gt;早期我容易把项目成果直接放进个人产出。后来负责的事情变多，才更清楚一个项目如期上线，可能同时依赖产品梳理需求、测试覆盖边界、上下游配合改造和团队成员完成各自部分。&lt;/p&gt;
&lt;p&gt;个人价值并没有因此消失。它可能体现在提出关键方案、推动分歧解决、提前发现风险，或者在复杂情况下保持项目继续向前。但这些贡献需要放回协作关系中说明，不能把整个结果都归于自己。&lt;/p&gt;
&lt;p&gt;同样，项目出了问题，也不能只用“协作方不给力”解释。负责人不一定要承担所有错误，却需要说明自己掌握了哪些信息、采取了什么行动，以及下一次可以改变什么。&lt;/p&gt;
&lt;h2 id="述职也暴露自我评价的偏差"&gt;述职也暴露自我评价的偏差&lt;/h2&gt;
&lt;p&gt;我过去很容易详细说明不足，却较少直接确认自己的贡献。&lt;/p&gt;
&lt;p&gt;做成一件事时，会想到平台、团队和机会的帮助；事情没有做好时，则首先检查自己是否不够主动、判断不够准确。这种习惯能带来行动上的改进，也可能让评价失去平衡。&lt;/p&gt;
&lt;p&gt;述职要求一个人暂时站到自己的工作之外，既不夸大，也不回避。哪些结果确实由自己的判断推动，哪些来自团队共同完成，哪些只是赶上了合适的条件，都应该分别说明。&lt;/p&gt;
&lt;p&gt;准确评价自己的工作，不等于抢功。一个人如果长期说不清自己的作用，也很难判断下一阶段应该继续发挥什么、补足什么。&lt;/p&gt;
&lt;h2 id="不再把述职当作年末整理"&gt;不再把述职当作年末整理&lt;/h2&gt;
&lt;p&gt;几次述职以后，我逐渐意识到，真正困难的不是制作最后那份文档。&lt;/p&gt;
&lt;p&gt;如果目标一开始就不清楚，项目过程中没有记录关键决定，完成后也没有观察结果，到了年末只能依靠项目清单和记忆拼出一份总结。这样的述职可能内容很多，却很难形成判断。&lt;/p&gt;
&lt;p&gt;更有效的做法，是在日常工作中保留少量记录：问题是什么，为什么这样做，结果如何，还有哪些没有解决。它既服务于汇报，也帮助自己避免重复犯错。&lt;/p&gt;
&lt;p&gt;现在回看旧材料，我仍然能看到一些当时特有的表达：急于证明技术深度，习惯用工作量说明价值，也会把未来计划列得很满。它们并不成熟，却真实记录了那个阶段如何理解工作。&lt;/p&gt;
&lt;p&gt;述职的形式没有那么重要。重要的是，一个人能否把做过的事情放回具体背景中，说明自己的作用，也承认结果受到哪些条件限制。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
技术方案里的妥协</title><link>https://bug1024.com/posts/compromises-in-technical-design/</link><guid isPermaLink="true">https://bug1024.com/posts/compromises-in-technical-design/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;技术方案讨论中，“硬编码”通常不是一个好词。&lt;/p&gt;
&lt;p&gt;它意味着规则直接写进程序，变化时需要重新开发和发布。相比之下，配置化看起来更加灵活，也更接近一套可以长期扩展的设计。&lt;/p&gt;
&lt;p&gt;我曾经在一个动态表单项目里遇到过类似选择。系统需要根据不同客户、业务阶段和用户角色展示不同字段。最初设想是建立一套通用配置，尽量让后续变化不再修改代码。&lt;/p&gt;
&lt;p&gt;真正进入实现以后，单纯配置很快遇到了边界。&lt;/p&gt;
&lt;h2 id="配置能够描述字段却难以描述所有关系"&gt;配置能够描述字段，却难以描述所有关系&lt;/h2&gt;
&lt;p&gt;字段名称、类型、是否必填和展示顺序，都很适合配置。&lt;/p&gt;
&lt;p&gt;问题出现在字段之间的联动。选择一个值以后，另一个字段可能出现、隐藏或改变校验规则；某些字段只在特定流程节点生效，还会受到角色和历史状态影响。&lt;/p&gt;
&lt;p&gt;如果继续追求全部配置化，就需要设计条件表达式、变量引用、优先级和冲突处理。配置系统开始拥有自己的语法和执行逻辑，实际上变成了一门小型编程语言。&lt;/p&gt;
&lt;p&gt;这套能力并非不能建设，但它的开发、测试和使用成本已经超出当时项目需要解决的问题。&lt;/p&gt;
&lt;h2 id="最后的方案不够纯粹"&gt;最后的方案不够纯粹&lt;/h2&gt;
&lt;p&gt;我们最终使用了配置、模板和少量明确代码相结合的方式。&lt;/p&gt;
&lt;p&gt;稳定、重复出现的字段能力进入配置；几类结构相似的场景由模板承载；少数业务含义特殊、短期内也不太可能复用的联动规则，则保留在代码中。&lt;/p&gt;
&lt;p&gt;从形式上看，这个方案不如“所有规则都可配置”整齐。它要求维护者知道三种机制各自负责什么，也没有彻底消除后续开发。&lt;/p&gt;
&lt;p&gt;但它控制了项目范围，避免为了尚未出现的需求建设一整套规则平台，也让关键业务逻辑仍然能够通过代码评审和测试得到明确约束。&lt;/p&gt;
&lt;h2 id="妥协不等于随意"&gt;妥协不等于随意&lt;/h2&gt;
&lt;p&gt;技术方案总是在约束中产生。交付时间、团队经验、业务稳定性和未来变化概率，都会影响选择。&lt;/p&gt;
&lt;p&gt;所谓妥协，不应该是“时间不够，先随便做”。有效的妥协需要说明放弃了什么、承担什么成本，以及哪些条件变化以后应该重新设计。&lt;/p&gt;
&lt;p&gt;如果选择硬编码，就要明确它适用的业务边界，避免分支散落在各处；如果选择配置化，就要为校验、版本和可观察性负责；如果使用模板，则要防止模板复制后逐渐产生无法合并的差异。&lt;/p&gt;
&lt;p&gt;每一种灵活性都有自己的维护成本。&lt;/p&gt;
&lt;h2 id="不为不确定的未来提前支付全部成本"&gt;不为不确定的未来提前支付全部成本&lt;/h2&gt;
&lt;p&gt;架构设计经常需要考虑未来，但未来需求并不总能准确预测。&lt;/p&gt;
&lt;p&gt;为了可能出现的变化提前建设通用平台，很容易得到一套能力很强、实际使用很少的系统。相反，完全忽略未来，也会让下一次扩展只能推翻重来。&lt;/p&gt;
&lt;p&gt;更现实的做法，是区分已经反复出现的变化与仅存在于想象中的变化。前者值得抽象，后者可以先保留清楚的扩展位置，而不必立即实现全部能力。&lt;/p&gt;
&lt;h2 id="好方案不一定形式最漂亮"&gt;好方案不一定形式最漂亮&lt;/h2&gt;
&lt;p&gt;现在回看那次选择，我仍然不会把它称为最优方案。它只是当时条件下能够解释的一种取舍。&lt;/p&gt;
&lt;p&gt;技术判断的价值，不是永远选出最优雅的结构，而是知道复杂度被放在了哪里，为什么愿意承担，以及什么时候需要重新评估。&lt;/p&gt;
&lt;p&gt;如果一个方案只能用“更先进”或“更灵活”解释，往往还没有真正说明它解决了什么问题。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
技术工作为什么难以被看见</title><link>https://bug1024.com/posts/why-technical-work-is-hard-to-see/</link><guid isPermaLink="true">https://bug1024.com/posts/why-technical-work-is-hard-to-see/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;新功能上线以后，变化通常很容易被看见。页面增加了入口，用户能够完成新的操作，业务数据也可能随之变化。&lt;/p&gt;
&lt;p&gt;技术团队的另一部分工作没有这么直观。一次容量治理让高峰期没有发生故障，一项迁移没有影响已有用户，一轮重构让之后的需求更容易开发。它们的结果常常表现为“什么都没有发生”。&lt;/p&gt;
&lt;p&gt;这让技术价值很容易陷入两个极端：要么无人注意，要么只能依靠大量专业指标证明。&lt;/p&gt;
&lt;h2 id="工作量不是价值说明"&gt;工作量不是价值说明&lt;/h2&gt;
&lt;p&gt;代码行数、需求数量、接口数量和处理数据规模，都能说明工作量或系统规模，却不能独立说明价值。&lt;/p&gt;
&lt;p&gt;一个复杂方案可能解决了关键问题，也可能只是系统本身过于复杂。一次性能提升如果原本没有业务瓶颈，数字再明显也未必重要。&lt;/p&gt;
&lt;p&gt;技术工作需要先回到问题：原来发生了什么，造成什么影响，为什么值得投入。然后再说明采取的行动和结果。&lt;/p&gt;
&lt;p&gt;指标在这个结构中提供证据，而不是代替结论。&lt;/p&gt;
&lt;h2 id="预防性工作尤其难说明"&gt;预防性工作尤其难说明&lt;/h2&gt;
&lt;p&gt;风险治理的价值来自对未来事件的降低，但未来本来就不确定。&lt;/p&gt;
&lt;p&gt;如果治理后没有发生事故，很难证明事故原本一定会发生；如果什么都不做也没有出事，又容易觉得投入没有必要。&lt;/p&gt;
&lt;p&gt;因此，预防性工作不能只依靠假设中的损失。更可靠的依据包括已经出现的容量趋势、重复故障、明确单点、恢复时间和演练结果。它们说明风险真实存在，也能验证治理是否改变了条件。&lt;/p&gt;
&lt;h2 id="平台建设不能只展示功能"&gt;平台建设不能只展示功能&lt;/h2&gt;
&lt;p&gt;技术平台容易用支持多少能力、接入多少组件来说明产出。&lt;/p&gt;
&lt;p&gt;真正有意义的问题是：多少团队实际使用，接入成本是否下降，重复建设是否减少，问题发现和处理是否更快。没有使用结果的平台能力，只能说明已经提供，不能说明已经产生价值。&lt;/p&gt;
&lt;p&gt;这要求平台团队不仅建设功能，也观察使用过程。&lt;/p&gt;
&lt;h2 id="说明价值不是抢功"&gt;说明价值不是抢功&lt;/h2&gt;
&lt;p&gt;有些技术人员不愿主动说明工作，担心显得在包装自己。另一些表达又容易把团队结果全部归到个人。&lt;/p&gt;
&lt;p&gt;更准确的方式，是区分个人作用与共同结果。说明自己负责了哪个判断、推动了什么改变，同时交代产品、测试、运维和其他开发者提供的条件。&lt;/p&gt;
&lt;p&gt;事实越具体，越不需要夸张措辞。&lt;/p&gt;
&lt;h2 id="日常记录比年末回忆可靠"&gt;日常记录比年末回忆可靠&lt;/h2&gt;
&lt;p&gt;技术价值如果只在述职时整理，很容易退化成项目清单。&lt;/p&gt;
&lt;p&gt;在项目过程中保留少量信息更有效：为什么开始，关键选择是什么，上线前后的结果如何，还有哪些成本没有解决。这些记录既方便复盘，也能避免后来只记得实现细节。&lt;/p&gt;
&lt;p&gt;技术工作并非天然不可见。只是它的价值往往不在功能表面，需要有人把问题、行动和结果之间的关系说明清楚。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
技术评审不只是评审技术</title><link>https://bug1024.com/posts/what-a-technical-review-reviews/</link><guid isPermaLink="true">https://bug1024.com/posts/what-a-technical-review-reviews/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;刚开始参加技术评审时，我主要关心方案本身：表怎么设计，接口如何定义，缓存是否需要，性能能不能满足要求。&lt;/p&gt;
&lt;p&gt;这些当然是技术评审的一部分。但项目做得多了以后，我发现评审中暴露出来的很多问题，其实并不发生在技术层面。&lt;/p&gt;
&lt;p&gt;有人对需求的理解不同，有人不知道系统还依赖另一个团队，有些异常流程从未被讨论，也有一些关键决定只存在于几个人的聊天记录里。技术方案只是把这些问题集中呈现了出来。&lt;/p&gt;
&lt;h2 id="写文档首先是在整理自己的思路"&gt;写文档首先是在整理自己的思路&lt;/h2&gt;
&lt;p&gt;一个方案只存在于脑中时，很容易显得完整。真正开始写文档，才会遇到具体问题：数据从哪里来，状态由什么条件改变，失败后是否重试，新旧接口怎样兼容，上线顺序如何安排。&lt;/p&gt;
&lt;p&gt;写作迫使思考变慢。原来被一句“这里做一下处理”带过的地方，需要变成可以被别人理解和检查的步骤。&lt;/p&gt;
&lt;p&gt;因此，技术文档的第一个读者通常是作者自己。它不一定要长，也不必套用固定模板，但应该让实现方案从头到尾走得通。对于状态复杂的功能，一张状态图可能比几页文字更有效；涉及多个系统时，时序图则能较快暴露调用顺序和异常处理上的空白。&lt;/p&gt;
&lt;p&gt;如果文档只能记录已经确定的结论，而不能帮助作者发现尚未确定的问题，它的作用就很有限。&lt;/p&gt;
&lt;h2 id="评审也是一次需求确认"&gt;评审也是一次需求确认&lt;/h2&gt;
&lt;p&gt;技术人员很容易认为，需求评审已经结束，技术评审只讨论如何实现。但不少返工恰恰来自“如何实现”背后隐藏的需求分歧。&lt;/p&gt;
&lt;p&gt;例如，同一个字段在产品眼里可能只是展示信息，在研发设计中却会影响状态流转；一个看似简单的配置项，可能同时涉及权限、历史数据和多个客户端版本。如果只检查代码层面的可行性，方案即使技术上成立，也未必符合最初要解决的问题。&lt;/p&gt;
&lt;p&gt;产品、测试、前端和后端参与评审，不是为了让会议显得正式。他们分别掌握不同的信息：产品确认目标和边界，测试补充异常场景，前端确认交互与协议，后端说明数据和系统约束。&lt;/p&gt;
&lt;p&gt;技术评审的重要结果之一，是确认大家讨论的是同一件事。&lt;/p&gt;
&lt;h2 id="把风险提前变成可以讨论的问题"&gt;把风险提前变成可以讨论的问题&lt;/h2&gt;
&lt;p&gt;项目中的很多风险并非无法预见，只是没有在合适的时间被说出来。&lt;/p&gt;
&lt;p&gt;外部系统不稳定怎么办，数据量增长后方案是否仍然成立，旧版本客户端会怎样处理新字段，上线时某个依赖方没有同步发布会发生什么。这些问题如果在评审阶段提出，可能只需要调整设计或上线计划；到了生产环境，便会变成故障和数据修复。&lt;/p&gt;
&lt;p&gt;评审不能消除所有风险。它更现实的作用，是让团队知道风险在哪里、由谁关注，以及发生以后如何处理。&lt;/p&gt;
&lt;p&gt;这也是为什么我后来会把上线计划放进技术方案。开发完成并不代表工作结束。配置、数据初始化、依赖顺序、灰度范围、监控和回滚方式，都属于方案的一部分。&lt;/p&gt;
&lt;h2 id="文档保存的是当时的判断"&gt;文档保存的是当时的判断&lt;/h2&gt;
&lt;p&gt;系统维护几年以后，代码只能告诉后来的人“最后怎样实现”，却不一定能解释“当初为什么这样实现”。&lt;/p&gt;
&lt;p&gt;也许当时有两个方案，最终因为时间、数据规模或依赖条件选择了其中一个；也许一个看起来不够简洁的兼容逻辑，是为了保证迁移期间不影响旧业务。背景消失以后，合理的妥协很容易被误认为随意设计，临时限制也可能被当成长期规则保留下来。&lt;/p&gt;
&lt;p&gt;一份有效的评审记录应该保存关键选择和约束，而不只是最终的表结构与接口。这样，新成员接手时不必完全依赖口头讲解，后续重构也能知道哪些条件已经发生变化。&lt;/p&gt;
&lt;h2 id="评审的尺度"&gt;评审的尺度&lt;/h2&gt;
&lt;p&gt;并不是所有需求都需要完整的设计文档和正式会议。改动很小、边界清楚的工作，如果也要求填写大量模板，评审本身就会成为负担。&lt;/p&gt;
&lt;p&gt;是否需要评审，可以看几件事：参与方是否较多，状态和数据是否复杂，是否存在兼容与迁移，失败后的影响是否明显，以及团队内部是否已经形成一致理解。&lt;/p&gt;
&lt;p&gt;技术评审的目的不是留下“已经评审过”的记录，而是减少那些本可以提前发现的误解和风险。&lt;/p&gt;
&lt;p&gt;方案最终仍然要由代码实现。但在代码之前，团队需要先把问题说清楚。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
技术深度曾经意味着什么</title><link>https://bug1024.com/posts/what-technical-depth-meant/</link><guid isPermaLink="true">https://bug1024.com/posts/what-technical-depth-meant/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;转向 Java 之后的几年，我花了很多时间学习 JVM、垃圾回收、多线程、字节码和框架源码。&lt;/p&gt;
&lt;p&gt;那时我对技术深度的理解相对直接：不能只会使用工具，还要知道它为什么这样工作。知道得越靠近底层，处理问题时就越不容易停留在表面。&lt;/p&gt;
&lt;p&gt;这个判断至今仍有价值，但已经不能完整解释我所理解的技术能力。&lt;/p&gt;
&lt;h2 id="从会用到知道原理"&gt;从会用到知道原理&lt;/h2&gt;
&lt;p&gt;刚开始工作时，很多问题可以通过查文档解决。框架怎样配置，接口如何调用，数据库语句怎么写，都有相对明确的答案。&lt;/p&gt;
&lt;p&gt;系统变大以后，只知道使用方式开始显得不够。线程为什么阻塞，内存为什么持续增长，一项配置为什么能改变性能，这些问题需要继续向下理解。&lt;/p&gt;
&lt;p&gt;学习底层原理带来的最大变化，并不是记住更多名词，而是能够建立一条解释链。看到现象以后，可以从应用代码追到框架、虚拟机和操作系统，知道下一步应该收集什么证据。&lt;/p&gt;
&lt;p&gt;我第一次处理 JVM 直接崩溃时，过去积累的知识确实帮助我理解 crash 文件和运行时异常。即便最终根因来自第三方序列化组件，而不是 JVM 自身，没有那些基础也很难缩小范围。&lt;/p&gt;
&lt;h2 id="知道原理不等于解决问题"&gt;知道原理不等于解决问题&lt;/h2&gt;
&lt;p&gt;不过，技术知识越多，也越容易把问题带到自己熟悉的方向。&lt;/p&gt;
&lt;p&gt;遇到性能异常时，熟悉 JVM 的人会先看 GC，熟悉数据库的人会先看 SQL，熟悉架构的人则可能急着讨论拆分。每个方向都有道理，但真实问题并不关心个人最擅长什么。&lt;/p&gt;
&lt;p&gt;一次排查中，能够列出十种可能性不代表更有深度。更重要的是根据现象建立假设，通过数据排除错误方向，并在事实变化时及时修正判断。&lt;/p&gt;
&lt;p&gt;原理知识提供可能性，问题分析决定先验证哪一种。&lt;/p&gt;
&lt;h2 id="技术方案还受到现实约束"&gt;技术方案还受到现实约束&lt;/h2&gt;
&lt;p&gt;后来参与更多系统设计，我发现技术深度也体现在取舍上。&lt;/p&gt;
&lt;p&gt;一个方案可能在结构上更完整，却需要更长交付时间；一项通用能力能够支持未来变化，也可能让当前系统过早承担复杂度。知道多种方案只是开始，还要说明每种方案在具体条件下的成本。&lt;/p&gt;
&lt;p&gt;这种判断无法完全从书本和源码中得到。它来自真实系统的演进、失败和维护，也来自对业务与团队能力的理解。&lt;/p&gt;
&lt;h2 id="深入和广泛并不矛盾"&gt;深入和广泛并不矛盾&lt;/h2&gt;
&lt;p&gt;过去我会把深入某项技术与关注业务、协作看成不同方向。现在更愿意把它们放在同一个问题里。&lt;/p&gt;
&lt;p&gt;技术细节决定方案能否成立，业务目标决定问题是否值得解决，组织条件则影响方案能否落地。缺少任何一部分，判断都可能失真。&lt;/p&gt;
&lt;p&gt;这并不意味着技术人员不再需要钻研底层。恰恰相反，只有具备可靠的专业判断，才能在复杂讨论中说明哪些限制真实存在，哪些只是习惯。&lt;/p&gt;
&lt;h2 id="现在怎样理解技术深度"&gt;现在怎样理解技术深度&lt;/h2&gt;
&lt;p&gt;现在我仍然尊重那些愿意读源码、追机制的人。很多困难问题最终都需要有人继续向下走。&lt;/p&gt;
&lt;p&gt;只是我不再用知道多少底层细节单独衡量技术深度。它还包括识别真正的问题，理解方案成立的条件，承认不确定性，并对选择带来的长期成本保持清楚。&lt;/p&gt;
&lt;p&gt;技术深度不是离业务和现实越远越深，而是在面对具体问题时，能够走到足以作出可靠判断的位置。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
监控不是安装一套工具</title><link>https://bug1024.com/posts/monitoring-is-not-a-tool/</link><guid isPermaLink="true">https://bug1024.com/posts/monitoring-is-not-a-tool/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;做过一段时间监控平台以后，我发现团队谈起监控时，经常首先讨论工具。&lt;/p&gt;
&lt;p&gt;使用哪种指标协议，数据存在哪里，怎样绘制图表，报警引擎支持什么语法。这些问题都需要解决，但工具部署完成，并不代表系统已经具备有效监控。&lt;/p&gt;
&lt;p&gt;真正的差别发生在数据产生以后。&lt;/p&gt;
&lt;h2 id="有指标不等于有人看"&gt;有指标不等于有人看&lt;/h2&gt;
&lt;p&gt;很多项目接入监控以后，确实能够看到接口耗时、错误量、线程池和垃圾回收等指标。但只有出现问题时，大家才临时打开页面寻找线索。&lt;/p&gt;
&lt;p&gt;这样的监控更接近排查工具。它能帮助解释故障，却不能提前发现故障。&lt;/p&gt;
&lt;p&gt;要让指标进入日常运行，还需要明确什么变化属于异常，报警发送给谁，收到以后怎样判断是否需要处理。否则，图表再完整，也只是被动保存系统状态。&lt;/p&gt;
&lt;h2 id="报警规则需要降低使用门槛"&gt;报警规则需要降低使用门槛&lt;/h2&gt;
&lt;p&gt;如果每个项目都从头理解指标、编写规则和配置接收人，接入成本会长期存在。&lt;/p&gt;
&lt;p&gt;我们后来为常见问题提供默认报警，例如异常增长、线程池耗尽和频繁 Full GC，并把报警接收人与项目责任关系连接起来。业务团队仍然可以配置更具体的规则，但基础风险不必每次重新建设。&lt;/p&gt;
&lt;p&gt;这种默认能力并不复杂，却比增加更多图表更直接地改变了问题发现方式。&lt;/p&gt;
&lt;p&gt;平台的作用不只是提供灵活性，也应该把已经验证过的经验变成默认设置。&lt;/p&gt;
&lt;h2 id="多套系统会制造新的信息成本"&gt;多套系统会制造新的信息成本&lt;/h2&gt;
&lt;p&gt;大型组织通常不只有一套监控。前端、客户端、服务端、日志和链路追踪可能来自不同平台。&lt;/p&gt;
&lt;p&gt;每套工具分别看都合理，真正排查问题时却需要在多个入口之间切换。项目名称、时间范围和责任关系如果不统一，使用者需要先完成一轮信息对齐。&lt;/p&gt;
&lt;p&gt;因此，我们曾经尝试收拢入口，让不同监控能力能够围绕同一个项目被找到。统一入口没有消除底层系统差异，只是减少“应该去哪里看”的成本。&lt;/p&gt;
&lt;h2 id="监控覆盖的是业务失败还是技术异常"&gt;监控覆盖的是业务失败还是技术异常&lt;/h2&gt;
&lt;p&gt;CPU、内存和接口错误容易监控，业务失败则更容易遗漏。&lt;/p&gt;
&lt;p&gt;一次请求返回成功，不代表用户目标已经完成；消息正常消费，也不代表状态最终一致。关键业务需要建立自己的过程与结果指标，技术指标则帮助解释这些结果为什么变化。&lt;/p&gt;
&lt;p&gt;只监控机器，系统可能在业务已经失败时仍然全部显示绿色。&lt;/p&gt;
&lt;h2 id="报警之后仍然需要流程"&gt;报警之后仍然需要流程&lt;/h2&gt;
&lt;p&gt;报警只是把异常送到人面前。是否及时处理，还取决于责任是否清楚、信息是否足够，以及团队是否知道怎样升级问题。&lt;/p&gt;
&lt;p&gt;报警过多会让人逐渐忽略，报警缺少上下文则会增加排查时间。没有定期清理和复盘的规则，最终都会失去作用。&lt;/p&gt;
&lt;p&gt;所以，监控体系同时涉及技术和组织：平台负责可靠地采集与计算，业务团队定义真正重要的信号，责任人处理异常，复盘再把新的经验补回规则。&lt;/p&gt;
&lt;h2 id="工具只是其中一层"&gt;工具只是其中一层&lt;/h2&gt;
&lt;p&gt;监控工具当然重要。没有稳定的数据链路，后续机制无从建立。&lt;/p&gt;
&lt;p&gt;但衡量监控能力时，我更关心几个结果：问题是否更早发现，接收人是否明确，排查是否更快，以及相同问题是否通过规则沉淀下来。&lt;/p&gt;
&lt;p&gt;安装一套工具可以很快完成。让监控真正进入系统的日常运行，需要更长时间。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
旧文五则：2010</title><link>https://bug1024.com/posts/old-writings-2010/</link><guid isPermaLink="true">https://bug1024.com/posts/old-writings-2010/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;整理旧资料时，发现了几篇以前写在QQ空间里的文章，稍作整理后留在这里。&lt;/p&gt;
&lt;h2 id="何人何处"&gt;何人何处&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;2010年2月16日&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;未雨，静悄悄，些许寒意。无风，冷清清，几缕愁思。辗转，夜无眠，留得憔颜。天涯，何处？&lt;/p&gt;
&lt;h2 id="等"&gt;等&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;2010年6月10日&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;微寒，起风了，揉乱了月色。犬吠，偶有猫叫，远处传来匆忙的脚步声。旧事与故人渐远，影子也慢慢变得模糊。昨日或喜或悲，都已经过去，不必停留太久。&lt;/p&gt;
&lt;p&gt;乐声将夜空分成两半，仿佛一条河，时间便是岸。我的桨划过很远，仍想知道，渡口是否有人在等。&lt;/p&gt;
&lt;h2 id="家本无情"&gt;家本无情&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;2010年10月3日&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;小雨，起了雾，回了趟家。家人外出，时间被冷落了，家里自然有了霉味。家变得陌生，面对着一样陌生的我。&lt;/p&gt;
&lt;p&gt;门前细水，桥边古木，桃红巷中桃花醉。檐下蛛丝，墙上鼠迹，苔青径里苔迹生。风雨弄离人，面湿未曾觉。何处是个家？一声春燕鸣。&lt;/p&gt;
&lt;p&gt;听雨轻叩瓦，我独自徘徊在屋里，心里空空的，里边回荡着雨声。家本无情？无情的是我吧！&lt;/p&gt;
&lt;h2 id="来去兮"&gt;来去兮&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;2010年10月6日&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;风来兮，雁去兮，不留人兮！人去兮，秋来兮，虫鱼散兮！来去兮，来去梦兮！&lt;/p&gt;
&lt;p&gt;梦兮梦兮……&lt;/p&gt;
&lt;h2 id="鱼又消失"&gt;鱼又消失&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;2010年10月6日&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;谁／又摔下一抹夜色／融进水里／碎了／惊了鱼儿／一摆尾／消失了／却忘了带走那一圈／仍在荡开的涟漪&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%97%A7%E6%96%87/">旧文</category></item><item><title>
开源软件出了问题以后</title><link>https://bug1024.com/posts/when-open-source-software-fails/</link><guid isPermaLink="true">https://bug1024.com/posts/when-open-source-software-fails/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;使用开源软件时，大部分问题都能在文档、Issue 或新版本中找到答案。&lt;/p&gt;
&lt;p&gt;真正困难的是另一种情况：线上出现了严重异常，初步证据指向某个开源组件，却无法稳定复现，社区也没有现成结论。&lt;/p&gt;
&lt;p&gt;我曾在处理 JVM 崩溃时遇到过这种问题。最终线索指向一个序列化组件，我们向相关项目反馈，也替换了原有实现。但从怀疑组件到确认问题，中间仍有很长距离。&lt;/p&gt;
&lt;h2 id="怀疑不是缺陷报告"&gt;怀疑不是缺陷报告&lt;/h2&gt;
&lt;p&gt;线上现象复杂，开源组件只是调用链中的一部分。&lt;/p&gt;
&lt;p&gt;应用使用方式、版本组合、运行环境和数据内容都可能影响结果。仅凭“替换以后不再发生”，可以支持判断，却未必足以解释根因。&lt;/p&gt;
&lt;p&gt;向社区报告问题前，需要尽量提供版本、环境、错误日志、触发条件和已经完成的排查。如果能够构造最小复现，问题被理解和修复的概率会高很多。&lt;/p&gt;
&lt;p&gt;这些工作不是替开源作者免费测试，而是让自己的判断从猜测变成可验证信息。&lt;/p&gt;
&lt;h2 id="官方没有义务按业务时间响应"&gt;官方没有义务按业务时间响应&lt;/h2&gt;
&lt;p&gt;开源项目的维护资源有限。企业线上问题再紧急，也不代表社区能够立即处理。&lt;/p&gt;
&lt;p&gt;提交 Issue 或错误报告以后，业务团队仍然需要自己的应对方案：降低影响、回退版本、关闭相关能力、增加隔离，或者替换组件。&lt;/p&gt;
&lt;p&gt;等待官方修复可以是一项计划，不能成为唯一计划。&lt;/p&gt;
&lt;h2 id="升级绕过修复还是替换"&gt;升级、绕过、修复还是替换&lt;/h2&gt;
&lt;p&gt;遇到组件问题时，升级到新版本通常是成本较低的尝试。但新版本也可能带来兼容变化，且问题未必已经修复。&lt;/p&gt;
&lt;p&gt;临时绕过能够快速恢复业务，却会留下特殊配置或分支；自行修复最接近根因，但需要理解代码并承担维护；直接替换组件则可能影响数据兼容和整个调用链。&lt;/p&gt;
&lt;p&gt;选择取决于故障影响、复现稳定性、组件重要程度和团队维护能力，而不是哪种方式在技术上最彻底。&lt;/p&gt;
&lt;h2 id="使用开源也意味着承担集成责任"&gt;使用开源也意味着承担集成责任&lt;/h2&gt;
&lt;p&gt;开源软件降低了从零建设的成本，并不把系统责任转移给维护者。&lt;/p&gt;
&lt;p&gt;企业选择版本、配置和使用方式，也决定是否进行容量测试、故障演练和升级维护。组件进入生产系统以后，它就是整体架构的一部分。&lt;/p&gt;
&lt;p&gt;平台团队需要知道关键组件的替代方案和风险边界，至少不能在出现问题时才第一次寻找项目状态。&lt;/p&gt;
&lt;h2 id="反馈是工程闭环的一部分"&gt;反馈是工程闭环的一部分&lt;/h2&gt;
&lt;p&gt;如果问题已经被定位，只在内部绕过，其他使用者仍可能遇到相同故障。向社区提供复现、日志或修复建议，能够让一次排查产生更长的作用。&lt;/p&gt;
&lt;p&gt;反馈不一定每次都得到合并，也不必把所有内部细节公开。只要去除业务信息，保留足以判断问题的技术证据，就可能帮助项目改善。&lt;/p&gt;
&lt;p&gt;开源协作并不只发生在提交代码时。准确报告一个真实问题，同样是使用者能够作出的贡献。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/%E5%BC%80%E6%BA%90/">开源</category></item><item><title>
平台能力不是做出来就有人用</title><link>https://bug1024.com/posts/platforms-need-adoption/</link><guid isPermaLink="true">https://bug1024.com/posts/platforms-need-adoption/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;做技术平台时，很容易把主要精力放在能力建设上。&lt;/p&gt;
&lt;p&gt;支持更多协议，提供更灵活的配置，增加新的查询和报警方式。功能完成以后，平台团队会自然期待业务项目接入。&lt;/p&gt;
&lt;p&gt;实际情况往往没有那么顺利。业务团队即使认可平台价值，也可能迟迟没有行动。&lt;/p&gt;
&lt;h2 id="接入成本由谁承担"&gt;接入成本由谁承担&lt;/h2&gt;
&lt;p&gt;对平台团队来说，一份接入文档和一个 SDK 已经提供了完整能力。对业务团队来说，接入意味着理解文档、申请权限、修改代码、安排测试和上线，还要承担改造引起的风险。&lt;/p&gt;
&lt;p&gt;如果业务正忙，这项工作很容易排在功能需求之后。平台收益属于长期和整体，接入成本却由当前项目立即支付。&lt;/p&gt;
&lt;p&gt;因此，不能简单把“不接入”理解为业务团队不重视技术建设。需要先看平台是否把成本留给了使用者。&lt;/p&gt;
&lt;h2 id="默认集成比反复宣传更有效"&gt;默认集成比反复宣传更有效&lt;/h2&gt;
&lt;p&gt;我们后来尝试把通用监控能力集成进统一框架。新项目使用框架后，HTTP、数据库和缓存等基础指标便默认具备，不再需要逐项申请和改造。&lt;/p&gt;
&lt;p&gt;这种方式限制了部分自由，却显著降低了接入成本。业务团队只需要关注自己的自定义指标，而不是重复建设基础部分。&lt;/p&gt;
&lt;p&gt;平台推广中，默认值往往比功能数量更重要。一个合理默认能够让大多数人先获得基本能力，再为少数特殊场景提供扩展。&lt;/p&gt;
&lt;h2 id="文档也是产品的一部分"&gt;文档也是产品的一部分&lt;/h2&gt;
&lt;p&gt;平台团队熟悉内部概念，很容易写出只有自己能快速理解的文档。&lt;/p&gt;
&lt;p&gt;有效文档应该从使用者任务出发：什么情况下需要这项能力，最短接入路径是什么，如何确认成功，出错后去哪里查看。完整列出所有参数，不等于帮助别人完成接入。&lt;/p&gt;
&lt;p&gt;示例项目、常见问题和明确支持渠道，都属于平台使用体验，而不是附加工作。&lt;/p&gt;
&lt;h2 id="覆盖率不等于价值"&gt;覆盖率不等于价值&lt;/h2&gt;
&lt;p&gt;平台可以通过强制要求获得很高接入率，但接入并不代表使用。&lt;/p&gt;
&lt;p&gt;监控 SDK 已经部署，不代表团队查看指标；报警已经配置，也可能因为噪声过多被忽略。除了项目数量，还应该观察功能是否进入真实工作流程，是否减少了排查和重复建设成本。&lt;/p&gt;
&lt;p&gt;平台价值最终体现在使用者行为发生变化，而不是后台多了一条接入记录。&lt;/p&gt;
&lt;h2 id="平台团队也需要产品视角"&gt;平台团队也需要产品视角&lt;/h2&gt;
&lt;p&gt;技术平台面对的用户是其他研发团队。它同样需要理解用户目标、使用成本和反馈，只是用户就在公司内部。&lt;/p&gt;
&lt;p&gt;这不意味着无条件满足所有需求。平台仍然要保持边界，避免为单个团队不断定制。但在拒绝之前，需要判断问题来自特殊诉求，还是平台本身难以使用。&lt;/p&gt;
&lt;p&gt;能力做出来只是第一步。让合适的人以足够低的成本使用，并在工作中真正依赖它，才构成平台建设的完整结果。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
入职一家新公司以后</title><link>https://bug1024.com/posts/joining-a-new-company/</link><guid isPermaLink="true">https://bug1024.com/posts/joining-a-new-company/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我保留着几份不同年份的入职记录。&lt;/p&gt;
&lt;p&gt;最早的一份写于2014年。内容从安装系统、配置 Nginx、使用版本管理开始，几乎每学会一个命令都会记下来。后来入职另一家公司，笔记里开始出现开发流程、日志排查、消息队列和高并发。再往后，记录的重点变成业务历史、组织关系、系统边界和关键链路。&lt;/p&gt;
&lt;p&gt;这些变化不只是因为公司规模不同，也说明我对“熟悉一份工作”的理解发生了变化。&lt;/p&gt;
&lt;h2 id="第一次入职先让环境运行起来"&gt;第一次入职：先让环境运行起来&lt;/h2&gt;
&lt;p&gt;第一次实习时，陌生的东西太多，很难判断什么最重要。&lt;/p&gt;
&lt;p&gt;电脑环境需要配置，代码要从仓库拉取，需求通过项目管理工具流转，开发完成以后还要经历测试和上线。学校里写完能够运行的程序，只是公司软件开发流程中的一小段。&lt;/p&gt;
&lt;p&gt;那时最迫切的目标，是让自己具备基本的工作能力。环境能够启动，知道去哪里查日志，理解任务怎样从提出走到上线，才有可能参与实际开发。&lt;/p&gt;
&lt;p&gt;因此，笔记里保存了大量操作步骤。它们现在已经过时，却准确记录了当时面对工作的入口。&lt;/p&gt;
&lt;h2 id="后来开始先看系统"&gt;后来开始先看系统&lt;/h2&gt;
&lt;p&gt;有了一些经验以后，入职新公司不再需要从基础工具学起。注意力会自然转向项目结构：有哪些服务，数据怎样流动，系统依赖谁，出现问题应该从哪里开始排查。&lt;/p&gt;
&lt;p&gt;我曾经在刚入职时，根据代码和文档列出一系列改进建议。接口封装、配置方式、代码规范、监控和公共组件都能找到问题。&lt;/p&gt;
&lt;p&gt;现在回看，那些观察不一定错，但下结论有些早。一个看起来不合理的设计，可能来自历史迁移、组织分工或当时的交付条件。只看代码，可以发现现象，却不一定理解为什么系统会变成这样。&lt;/p&gt;
&lt;h2 id="再后来先理解业务历史"&gt;再后来先理解业务历史&lt;/h2&gt;
&lt;p&gt;进入更复杂的业务后，我逐渐把业务理解放在代码之前。&lt;/p&gt;
&lt;p&gt;先弄清楚平台服务谁，价值怎样产生，业务经历过哪些阶段，主流程由哪些角色参与。然后再看这些概念如何映射到系统，哪些模块属于核心链路，哪些只是特定阶段留下的分支。&lt;/p&gt;
&lt;p&gt;业务历史很重要。今天看似重复的两个系统，可能来自不同阶段的组织目标；一个难以理解的字段，也许曾经对应某个关键合作模式。忽略这些背景，很容易把历史形成的问题简单归因于前人的设计能力。&lt;/p&gt;
&lt;h2 id="文档与实际运行之间"&gt;文档与实际运行之间&lt;/h2&gt;
&lt;p&gt;新员工通常从文档开始，但文档永远不够完整。&lt;/p&gt;
&lt;p&gt;有些文档写的是设计目标，线上运行的却是经过多次调整后的版本；有些关键规则没有进入正式说明，只存在于团队成员的经验中。只读文档会得到一套整齐的系统，只读代码又容易迷失在细节里。&lt;/p&gt;
&lt;p&gt;更有效的方式，是带着一个真实流程交叉验证：用户完成一次核心操作会经过哪些系统，数据在哪里产生，异常怎样处理，最终由谁观察结果。沿着一条实际链路，文档、代码、监控和人的经验才能连接起来。&lt;/p&gt;
&lt;h2 id="熟悉组织也是工作的一部分"&gt;熟悉组织也是工作的一部分&lt;/h2&gt;
&lt;p&gt;系统边界往往与组织边界相互影响。&lt;/p&gt;
&lt;p&gt;哪个团队负责基础能力，发生故障时谁有决定权，一项改动需要哪些协作方，这些信息不会直接出现在代码中，却会决定项目怎样推进。&lt;/p&gt;
&lt;p&gt;入职之初不必急着经营大量关系，但需要知道关键问题应该找谁，团队通常怎样做决定，以及哪些事情必须通过正式流程完成。&lt;/p&gt;
&lt;h2 id="入职记录留下的变化"&gt;入职记录留下的变化&lt;/h2&gt;
&lt;p&gt;从工具、代码到业务和组织，我并没有找到一套适用于所有公司的固定方法。不同阶段仍然需要从不同位置进入。&lt;/p&gt;
&lt;p&gt;但现在再进入陌生环境，我不会急着证明自己已经看见问题。先理解一套系统为何如此运行，再判断哪些部分值得改变，通常比快速给出建议更重要。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
上线清单里应该有什么</title><link>https://bug1024.com/posts/what-belongs-in-a-release-checklist/</link><guid isPermaLink="true">https://bug1024.com/posts/what-belongs-in-a-release-checklist/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我曾经经历过一次并不顺利的新服务上线。&lt;/p&gt;
&lt;p&gt;代码和测试都已经完成，真正发布时却发现环境配置、外部依赖和数据准备没有完全对齐。单独看，每一项都不复杂；集中到上线窗口，任何遗漏都会让团队临时判断。&lt;/p&gt;
&lt;p&gt;后来再负责类似项目，我开始提前整理上线清单。&lt;/p&gt;
&lt;h2 id="代码不是唯一发布对象"&gt;代码不是唯一发布对象&lt;/h2&gt;
&lt;p&gt;一次功能上线可能同时包含应用代码、数据库变更、配置、定时任务、消息订阅、权限和初始化数据。&lt;/p&gt;
&lt;p&gt;这些内容通常由不同角色准备，也可能使用不同发布系统。如果只跟踪代码版本，其他变化很容易依赖口头确认。&lt;/p&gt;
&lt;p&gt;上线清单首先要回答：这次究竟有哪些东西发生变化，每项由谁执行，执行以后怎样确认成功。&lt;/p&gt;
&lt;h2 id="顺序本身就是方案的一部分"&gt;顺序本身就是方案的一部分&lt;/h2&gt;
&lt;p&gt;涉及上下游时，同样的改动按照不同顺序发布，结果可能完全不同。&lt;/p&gt;
&lt;p&gt;新接口是否兼容旧调用方，数据库字段应该先加还是后加，消费者能否识别新消息，配置在代码发布前生效会发生什么，都需要提前说明。&lt;/p&gt;
&lt;p&gt;“各团队同时上线”通常不是可靠方案。真实发布很难完全同步，因此系统应尽量允许新旧版本短暂共存。无法兼容时，则要明确暂停窗口和恢复条件。&lt;/p&gt;
&lt;h2 id="上线前要知道如何观察"&gt;上线前要知道如何观察&lt;/h2&gt;
&lt;p&gt;功能已经发布，页面能够访问，并不足以确认上线成功。&lt;/p&gt;
&lt;p&gt;需要提前确定关键观察项：错误量有没有变化，核心流程是否完成，消息是否积压，数据是否按预期生成。若等上线以后再临时寻找指标，最需要判断的时候反而缺少依据。&lt;/p&gt;
&lt;p&gt;监控与业务验证也应区分。接口返回成功不代表业务状态正确，技术指标稳定也不代表用户已经能够完成操作。&lt;/p&gt;
&lt;h2 id="回滚不能只写回滚版本"&gt;回滚不能只写“回滚版本”&lt;/h2&gt;
&lt;p&gt;应用代码通常可以切回旧版本，已经写入的新数据却未必能够自动恢复。数据库结构、消息和外部调用一旦发生，回滚可能比上线更复杂。&lt;/p&gt;
&lt;p&gt;清单需要说明哪些变化可逆，哪些只能向前修复。对于不可逆步骤，执行前应有额外确认，也要准备失败后的处理方式。&lt;/p&gt;
&lt;h2 id="清单不应无限增长"&gt;清单不应无限增长&lt;/h2&gt;
&lt;p&gt;每次事故后增加一项检查很容易，几年以后清单可能变得很长，其中许多内容已经被平台自动保证，或者不再适用于当前系统。&lt;/p&gt;
&lt;p&gt;可以自动验证的，应逐渐交给工具；只适用于特定项目的，不必成为全团队永久要求。清单需要定期删除，而不是只增加。&lt;/p&gt;
&lt;p&gt;它的作用不是证明流程已经执行，而是在上线前把容易被分散忽略的条件集中展示。&lt;/p&gt;
&lt;p&gt;一次可控发布，不是保证绝不出错，而是知道发生了什么变化，如何确认结果，以及出现偏差时怎样停止。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
十年学习记录</title><link>https://bug1024.com/posts/ten-years-of-learning/</link><guid isPermaLink="true">https://bug1024.com/posts/ten-years-of-learning/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;整理旧资料时，我找到了一批从2014年开始保存的学习笔记。&lt;/p&gt;
&lt;p&gt;最早的内容来自实习。里面记着如何配置 Nginx、怎样使用 Laravel、Linux 文件权限是什么意思，也记着一些现在看来已经没有必要保存的命令。那时接触到的东西几乎都会被写下来，知道得不多，记录得却很勤快。&lt;/p&gt;
&lt;p&gt;这些笔记一直写到2023年。单篇内容大多谈不上完整，有些只是一个链接，有些是几段复制下来的说明。不过把它们按年份放在一起，仍然能看出我的工作和学习是怎样变化的。&lt;/p&gt;
&lt;h2 id="先学会把事情做出来"&gt;先学会把事情做出来&lt;/h2&gt;
&lt;p&gt;2014年至2016年，我主要使用 PHP，也接触前端、数据库和服务器运维。学习通常从眼前的问题开始：框架怎么用，服务怎么部署，SQL 为什么慢，缓存如何配置。&lt;/p&gt;
&lt;p&gt;刚开始工作时，知识的价值很直接。今天学会一个命令，明天可能就会在服务器上用到；读懂一段框架代码，接下来便能少踩一个坑。因此，那几年的笔记范围很散，从 JavaScript 到 MySQL，再到 Nginx 和 Docker，什么都记。&lt;/p&gt;
&lt;p&gt;这种学习方式并不系统，却很符合当时的处境。先把问题解决，再逐渐补齐对问题的理解。很多工程师的起点大概都是如此。&lt;/p&gt;
&lt;h2 id="开始追问它为什么这样工作"&gt;开始追问它为什么这样工作&lt;/h2&gt;
&lt;p&gt;2017年转向 Java 以后，笔记里的问题明显变了。&lt;/p&gt;
&lt;p&gt;线程池为什么这样设计，垃圾回收器如何工作，索引为什么能提高查询速度，分布式系统怎样处理一致性。相比记住一个配置项，我开始花更多时间了解配置背后的机制。&lt;/p&gt;
&lt;p&gt;2018年至2019年，这种倾向更加明显。那段时间关注 Java 探针、字节码、JIT、堆外内存和性能诊断，也研究过监控系统和一些中间件。工作中遇到的系统更大，问题不再总能靠搜索一条命令解决。只有理解调用链路和运行机制，才知道应该从哪里开始排查。&lt;/p&gt;
&lt;p&gt;学习也从“以后可能有用”变成了“当前问题要求我继续向下追”。这是两种不同的动力。前者容易积累大量知识点，后者更容易形成真正属于自己的经验。&lt;/p&gt;
&lt;h2 id="技术之外的问题变多了"&gt;技术之外的问题变多了&lt;/h2&gt;
&lt;p&gt;2020年以后，纯技术记录开始减少。笔记里逐渐出现架构设计、技术评审、跨团队协作、项目推进和述职表达。&lt;/p&gt;
&lt;p&gt;这并不是因为技术问题变少了，而是工作中的困难已经不只来自代码。一个方案能否落地，还取决于需求是否清楚、边界如何划分、上下游是否达成一致，以及项目出现变化时由谁做决定。&lt;/p&gt;
&lt;p&gt;过去我容易把复杂问题理解为技术问题，后来才发现，很多故障在写代码之前就已经埋下了：信息没有同步，目标并不一致，临时方案没有退出计划，或者所有人都默认会有别人补位。&lt;/p&gt;
&lt;p&gt;技术仍然重要，只是它不再是解释所有问题的唯一视角。&lt;/p&gt;
&lt;h2 id="记录越来越少"&gt;记录越来越少&lt;/h2&gt;
&lt;p&gt;2021年之后，按月记录的内容明显变少。到2023年，全年只剩下几条与 AI 有关的笔记。&lt;/p&gt;
&lt;p&gt;一方面，搜索和问答工具让获取信息变得更快，不必再把每项知识搬进自己的笔记；另一方面，工作越熟悉，越容易觉得很多东西已经理解，没有必要专门写下来。&lt;/p&gt;
&lt;p&gt;现在回头看，这两种理由都只对了一部分。&lt;/p&gt;
&lt;p&gt;资料确实不必重复保存。大量旧笔记只是外部内容的副本，几年以后既没有继续使用，也很难确认是否仍然准确。但如果完全停止记录，消失的不只是资料，还有当时为何关注它、如何理解它，以及后来怎样修正判断的过程。&lt;/p&gt;
&lt;h2 id="这批笔记留下了什么"&gt;这批笔记留下了什么&lt;/h2&gt;
&lt;p&gt;十年间使用过的框架和工具已经换过很多轮。早期认真记录的一些技术，如今甚至很少再被提起。如果只从知识是否仍然有效来看，这批笔记的大部分内容都可以删除。&lt;/p&gt;
&lt;p&gt;但它们还保存了另一件事：一个人如何从掌握工具开始，逐渐理解系统，再把注意力转向工作中的协作与判断。&lt;/p&gt;
&lt;p&gt;以后我不会再按照从前的方式保存所有知识。查得到的资料不必搬运，未经消化的内容也不急着整理成文章。真正值得写下来的，是一个问题为什么会发生，我曾经怎样判断，事实又怎样改变了这个判断。&lt;/p&gt;
&lt;p&gt;这些内容未必比技术知识更正确，却更难从别处找到。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
收藏不等于学习</title><link>https://bug1024.com/posts/bookmarking-is-not-learning/</link><guid isPermaLink="true">https://bug1024.com/posts/bookmarking-is-not-learning/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我的旧学习笔记里保存了很多链接。&lt;/p&gt;
&lt;p&gt;技术博客、书单、开源项目、问答网站和公众号文章，按月份不断累积。有的链接后面只有一个标题，有的复制了几段正文，还有一些被重新整理进 Java、MySQL、监控和架构等专题。&lt;/p&gt;
&lt;p&gt;当时保存它们，是因为觉得以后还会用到。十年后重新打开，很多页面已经失效，更多内容则完全想不起为什么收藏。&lt;/p&gt;
&lt;p&gt;资料还在，学习却没有因此自动发生。&lt;/p&gt;
&lt;h2 id="保存信息带来的错觉"&gt;保存信息带来的错觉&lt;/h2&gt;
&lt;p&gt;看到一篇写得清楚的文章，把它放进笔记，往往会产生一种已经掌握的感觉。至少这个问题不再陌生，将来需要时也能找到。&lt;/p&gt;
&lt;p&gt;但收藏解决的是信息入口，不是理解。&lt;/p&gt;
&lt;p&gt;如果没有用自己的语言重新说明，没有把它和已有经验建立关系，也没有在真实问题中使用，它仍然只是别人的结论。过一段时间再看，通常需要从头理解一次。&lt;/p&gt;
&lt;p&gt;早期的月度笔记尤其明显。同一个文件里可能同时出现框架配置、数据库原理、工具命令和职业建议。它们反映了我当时关注过什么，却很难构成可以调用的知识。&lt;/p&gt;
&lt;p&gt;这并不意味着收藏没有意义。在搜索还没有今天这样方便的时候，一套经过筛选的资料确实能节省时间。问题是我曾经把资料数量误认为学习成果。&lt;/p&gt;
&lt;h2 id="从按时间记录到按专题整理"&gt;从按时间记录到按专题整理&lt;/h2&gt;
&lt;p&gt;后来我不再只按月份记录，开始把内容整理成专题。&lt;/p&gt;
&lt;p&gt;Java 基础、多线程、JVM 和垃圾回收被放到一起；监控则按照指标采集、存储、报警和展示重新分类。相比零散链接，专题能够呈现一项知识的结构，也更容易发现缺少的部分。&lt;/p&gt;
&lt;p&gt;这个变化是有效的。为了确定一段内容应该放在哪里，必须先判断它和其他内容的关系。重复的说法需要合并，互相矛盾的结论也会变得明显。&lt;/p&gt;
&lt;p&gt;不过，专题化仍然不能保证内容属于自己。一个目录可以整理得很完整，其中大部分依然可能来自书籍和外部文章。如果只是把别人写好的知识重新排列，得到的是一份更好检索的资料，而不是新的认识。&lt;/p&gt;
&lt;p&gt;资料库与文章的区别也在这里。资料库追求覆盖面，文章则需要说明作者看见了什么问题、依据什么形成判断。&lt;/p&gt;
&lt;h2 id="真正留下来的往往与问题有关"&gt;真正留下来的往往与问题有关&lt;/h2&gt;
&lt;p&gt;回看这些笔记，我还能清楚记得的内容，大多与具体问题相连。&lt;/p&gt;
&lt;p&gt;一次线上异常为什么迟迟无法定位，一套监控系统为何在资源没有耗尽时出现处理瓶颈，一个定制化需求怎样让原本清楚的模型变得复杂。为了处理这些问题读过的资料，即使没有完整记录，理解反而保留得更久。&lt;/p&gt;
&lt;p&gt;原因并不复杂。真实问题提供了边界：哪些信息重要，什么结论需要验证，方案受到什么条件限制。知识不再是孤立的名词，而是参与了一次判断。&lt;/p&gt;
&lt;p&gt;这也改变了我对“输出倒逼输入”的理解。输出并不只是把刚学到的内容再讲一遍。更重要的是，写作会暴露自己无法解释的地方，也迫使人区分引用、事实和个人判断。&lt;/p&gt;
&lt;p&gt;如果写完以后只得到一篇更顺畅的资料汇编，学习可能仍然停留在整理阶段。&lt;/p&gt;
&lt;h2 id="ai-让保存变得更没有必要"&gt;AI 让保存变得更没有必要&lt;/h2&gt;
&lt;p&gt;现在，获取一项知识比过去更容易。搜索可以找到资料，AI 也能快速归纳概念、比较方案并生成示例。继续手工保存大量通用知识，收益越来越低。&lt;/p&gt;
&lt;p&gt;但工具越方便，另一种问题越明显：答案来得太快，使用者很容易跳过形成问题和验证答案的过程。&lt;/p&gt;
&lt;p&gt;因此，我现在更愿意记录三类内容：遇到了什么具体问题，当时为什么作出某个选择，以及后来有什么事实修正了原来的认识。通用概念只保留必要的上下文，并注明来源。&lt;/p&gt;
&lt;p&gt;这些记录不追求建立一套无所不包的知识库。它们只是尽可能保存自己的判断过程。&lt;/p&gt;
&lt;h2 id="旧笔记如何处理"&gt;旧笔记如何处理&lt;/h2&gt;
&lt;p&gt;这次整理中，我没有把几百篇学习笔记全部迁入博客。纯技术摘录继续留在私人资料库，包含真实经历的内容则作为以后写作的素材。失效链接、过时配置和无法确认来源的段落，不再因为“也许有用”而公开保留。&lt;/p&gt;
&lt;p&gt;收藏、整理、理解和表达是几件不同的事。过去我经常把它们连在一起，现在更愿意分别看待。&lt;/p&gt;
&lt;p&gt;能找到一条信息并不困难。困难的是知道它在什么条件下成立，以及自己是否真的据此作出过判断。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
我做面试官时关注什么</title><link>https://bug1024.com/posts/what-i-look-for-in-interviews/</link><guid isPermaLink="true">https://bug1024.com/posts/what-i-look-for-in-interviews/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;刚开始参与技术面试时，我准备过一份很长的问题清单。&lt;/p&gt;
&lt;p&gt;Java 集合、多线程、JVM、数据库、缓存、消息队列和分布式系统，每个主题都能继续追问。问题准备得越多，似乎越能准确判断候选人的技术水平。&lt;/p&gt;
&lt;p&gt;实际面试多了以后，我越来越少按照清单逐项提问。&lt;/p&gt;
&lt;h2 id="面试不是看谁知道得更多"&gt;面试不是看谁知道得更多&lt;/h2&gt;
&lt;p&gt;面试官天然处于有利位置。问题由自己选择，也通常来自自己熟悉的领域。如果把候选人问到回答不出，并不能说明面试有效，更可能只是说明双方知识范围不同。&lt;/p&gt;
&lt;p&gt;知识基础当然需要确认。不同岗位也有必须掌握的内容。但孤立问题能够判断的，主要是候选人是否接触和记住过某个知识点。&lt;/p&gt;
&lt;p&gt;真正进入工作以后，更常见的情况是问题没有标准答案，信息也不完整。相比立即说出结论，我更关心一个人怎样确认问题、提出假设，以及发现原有判断不成立时是否愿意调整。&lt;/p&gt;
&lt;h2 id="从真实项目向下追问"&gt;从真实项目向下追问&lt;/h2&gt;
&lt;p&gt;我通常会请候选人介绍一个自己真正负责过的项目，再沿着其中的选择继续问。&lt;/p&gt;
&lt;p&gt;为什么要做，候选人负责哪一部分，遇到过什么限制，方案由谁决定，结果怎样验证。如果说做过性能优化，就问原来的瓶颈如何确认；如果提到架构升级，就问为什么不能继续使用旧方案。&lt;/p&gt;
&lt;p&gt;真实经历有上下文，也允许候选人进入自己熟悉的领域。一个项目未必规模很大，只要能够把问题、行动和结果说清楚，往往比背出复杂概念更有信息量。&lt;/p&gt;
&lt;h2 id="不知道答案时还能看什么"&gt;不知道答案时还能看什么&lt;/h2&gt;
&lt;p&gt;候选人遇到不会的问题很正常。&lt;/p&gt;
&lt;p&gt;有人会直接说明没有接触过，再尝试根据已有知识分析；有人会不断猜测，直到给出一个听起来确定的答案。两种反应体现的不是知识量，而是对认知边界的态度。&lt;/p&gt;
&lt;p&gt;工作中错误信息常常比暂时不知道更危险。能够区分事实、经验和推测，并说明需要什么信息继续判断，是一种重要能力。&lt;/p&gt;
&lt;h2 id="表达不流畅不等于思考不清楚"&gt;表达不流畅不等于思考不清楚&lt;/h2&gt;
&lt;p&gt;面试是一种高压、陌生且有时间限制的交流。性格内向、紧张或不擅长自我展示，都可能影响表现。&lt;/p&gt;
&lt;p&gt;因此我会尽量区分表达形式和思考内容。必要时换一种问法，给候选人一些整理时间，或者让他画出流程。面试官需要获取足够证据，而不是把第一次没有听懂直接当成能力不足。&lt;/p&gt;
&lt;p&gt;当然，工作本身需要沟通时，表达能力仍然属于评价范围。只是它应该根据岗位要求判断，而不是偏好外向和善于包装的人。&lt;/p&gt;
&lt;h2 id="面试官也需要被约束"&gt;面试官也需要被约束&lt;/h2&gt;
&lt;p&gt;面试前了解候选人经历，控制问题与岗位相关，不打听无关私人信息，也不把大部分时间用于讲述自己的项目，这些都是基本要求。&lt;/p&gt;
&lt;p&gt;一场面试无法完整判断一个人。它只是有限时间里的证据收集，因此结论应尽量来自具体表现，而不是“感觉不错”或“和我很像”。&lt;/p&gt;
&lt;p&gt;现在我仍会准备问题，但它们更像入口，而不是标准试卷。面试真正要判断的，不是候选人能否回答我知道的问题，而是我们是否看见了他进入真实工作以后可能怎样处理问题。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
一次复盘应该还原什么</title><link>https://bug1024.com/posts/restore-the-decision-context/</link><guid isPermaLink="true">https://bug1024.com/posts/restore-the-decision-context/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;线上问题发生以后，复盘会议里经常会出现一类问题：为什么当时没有发现，为什么没有提前提醒，为什么选择了这个方案。&lt;/p&gt;
&lt;p&gt;这些问题有必要问。没有追问，团队很难找到改进方向。但如果所有问题都以“为什么你没有”为开头，复盘很容易从分析事情如何发生，变成判断某个人是否尽责。&lt;/p&gt;
&lt;p&gt;结果已经发生以后，人会自然地觉得线索十分明显。真正需要还原的，是结果尚未发生时，参与者面对的是什么。&lt;/p&gt;
&lt;h2 id="当时知道什么"&gt;当时知道什么&lt;/h2&gt;
&lt;p&gt;复盘首先应该区分事后事实与当时信息。&lt;/p&gt;
&lt;p&gt;今天已经知道某项配置会引起故障，不代表设计时它就是明确风险；今天看到完整调用链，也不代表当时每个人都掌握全部依赖。&lt;/p&gt;
&lt;p&gt;需要重新列出当时可获得的信息：需求怎样描述，历史数据是多少，类似方案是否运行过，监控暴露了什么，还有哪些信息当时不存在。&lt;/p&gt;
&lt;p&gt;如果关键事实当时已经存在，却因为检查不足没有被识别，这是可以改进的判断问题。如果事实只能在事后获得，就不能用今天的确定性要求昨天的人提前知道答案。&lt;/p&gt;
&lt;h2 id="当时受到什么约束"&gt;当时受到什么约束&lt;/h2&gt;
&lt;p&gt;技术方案从来不是只在正确与错误之间选择。&lt;/p&gt;
&lt;p&gt;上线时间、可用人员、旧系统兼容、外部依赖和迁移成本都会影响决定。有时团队知道一个方案不够理想，仍然因为时间和风险选择它。这不一定意味着决定错误，但应该确认当时是否明确记录了代价。&lt;/p&gt;
&lt;p&gt;复盘时把约束重新列出来，不是替错误找理由，而是判断下一次应改变哪一项条件。若问题来自排期不合理，增加一条代码规范没有帮助；若风险来自信息没有同步，只要求开发更加谨慎也很难奏效。&lt;/p&gt;
&lt;h2 id="谁参与了判断"&gt;谁参与了判断&lt;/h2&gt;
&lt;p&gt;复杂项目很少由一个人独立决定。&lt;/p&gt;
&lt;p&gt;产品确定目标，技术评估实现，测试判断覆盖范围，负责人平衡时间和资源。最后的方案可能经过多轮讨论，也可能在一次临时沟通中仓促确定。&lt;/p&gt;
&lt;p&gt;复盘需要还原决策过程，而不是把结果压缩到最后执行的人身上。谁提供了什么信息，分歧如何解决，风险由谁接受，都应该尽量清楚。&lt;/p&gt;
&lt;p&gt;这样做并非分摊责任，而是找到系统真正缺少的机制。否则，每次复盘都以个人“以后注意”结束，相似问题仍会在另一位同事身上重演。&lt;/p&gt;
&lt;h2 id="风险是否被看见并接受"&gt;风险是否被看见并接受&lt;/h2&gt;
&lt;p&gt;并非所有风险都能在上线前消除。&lt;/p&gt;
&lt;p&gt;有些风险影响较小，可以通过监控和回滚承担；有些风险发生概率低，但解决成本极高，团队可能有意识地暂时接受。问题不在于系统存在风险，而在于风险是否经过讨论，是否有明确应对方式。&lt;/p&gt;
&lt;p&gt;如果风险已经被识别并由相应角色接受，复盘重点应放在原有判断是否合理。如果风险从未进入讨论，则要检查信息收集、评审和升级机制为什么失效。&lt;/p&gt;
&lt;h2 id="个人责任仍然存在"&gt;个人责任仍然存在&lt;/h2&gt;
&lt;p&gt;强调决策条件，不是为了否认个人责任。&lt;/p&gt;
&lt;p&gt;有人遗漏了本应完成的检查，有人知道风险却没有同步，有人在缺少依据时作出了过度承诺，这些都需要准确说明。只是个人问题也应该具体到行为，而不是用“责任心不足”概括一切。&lt;/p&gt;
&lt;p&gt;具体问题才有可能形成具体改进：增加检查项、调整权限、补充监控、明确决策人，或者让重大风险留下记录。&lt;/p&gt;
&lt;h2 id="复盘最终要改变条件"&gt;复盘最终要改变条件&lt;/h2&gt;
&lt;p&gt;一次有效复盘不必找到唯一责任人，也不一定能得到完整根因。但它至少应该让下一次面对相似问题时，团队拥有比上一次更多的信息和更好的条件。&lt;/p&gt;
&lt;p&gt;如果会议最后只留下“加强意识”“提高重视”，通常意味着问题仍然没有被还原清楚。&lt;/p&gt;
&lt;p&gt;复盘不是用今天的结果审判昨天。它要解释的是，在当时的条件下，事情为什么会沿着那条路径发生。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
一次旧系统的平滑迁移</title><link>https://bug1024.com/posts/migrating-a-legacy-system/</link><guid isPermaLink="true">https://bug1024.com/posts/migrating-a-legacy-system/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;一套监控系统运行多年以后，原有存储开始限制新的使用方式。&lt;/p&gt;
&lt;p&gt;它能够支持固定时间范围的报表，却难以进行灵活查询，也不方便向其他系统开放指标。我们准备引入新的时序存储，同时保留旧系统继续运行。&lt;/p&gt;
&lt;p&gt;真正困难的不是把数据写入新存储，而是怎样迁移已有的大量接入方。&lt;/p&gt;
&lt;h2 id="客户端升级还是服务端改造"&gt;客户端升级还是服务端改造&lt;/h2&gt;
&lt;p&gt;最直接的办法，是发布新版客户端，让业务项目逐步改为向新系统上报。&lt;/p&gt;
&lt;p&gt;它的好处是新旧链路边界清楚，服务端不需要承担额外处理。问题也同样明显：接入项目很多，升级节奏无法统一，有些项目长期不再维护。迁移可能持续很久，期间还要同时维护两套接入方式。&lt;/p&gt;
&lt;p&gt;另一种办法是在服务端改造。客户端继续按照原协议上报，服务端收到数据后同时写入新旧存储。业务方不需要配合，但双写会进入原有核心链路，可能影响正在运行的监控数据。&lt;/p&gt;
&lt;p&gt;我们最终选择了服务端方案。它把迁移风险集中到平台团队，也让迁移进度变得可控。&lt;/p&gt;
&lt;h2 id="双写很快成为新的瓶颈"&gt;双写很快成为新的瓶颈&lt;/h2&gt;
&lt;p&gt;最初实现是在原处理流程中直接把数据发送到消息队列，再由下游写入新存储。&lt;/p&gt;
&lt;p&gt;数据量上来以后，双写性能明显下降。原系统本来能够处理的流量，因为新增链路开始受到影响。&lt;/p&gt;
&lt;p&gt;后续优化分成几个方向：在发送前对指标进行预计算，减少原始数据量；把多条数据批量发送，降低网络与协议开销；使用更紧凑的序列化格式压缩消息；再将任务分片，避免处理集中在单一执行路径。&lt;/p&gt;
&lt;p&gt;这些优化没有改变迁移目标，只是在控制双写给旧系统带来的额外成本。&lt;/p&gt;
&lt;h2 id="迁移期间需要两套判断"&gt;迁移期间需要两套判断&lt;/h2&gt;
&lt;p&gt;双写以后，数据成功进入新系统并不代表迁移完成。&lt;/p&gt;
&lt;p&gt;需要持续比较新旧结果，确认指标数量、时间范围和聚合结果是否一致。新链路出现问题时，还要判断是上报、消息传输、计算还是存储造成。&lt;/p&gt;
&lt;p&gt;迁移期因此会暂时增加系统复杂度。旧逻辑不能立即删除，新逻辑又需要被完整监控。这个阶段如果没有明确结束条件，双写很容易从临时方案变成永久结构。&lt;/p&gt;
&lt;h2 id="平滑迁移不是没有风险"&gt;平滑迁移不是没有风险&lt;/h2&gt;
&lt;p&gt;“平滑”容易让人以为迁移过程不会发生问题。实际上，它只是把一次集中切换的风险，拆成更长时间内可观察、可回退的步骤。&lt;/p&gt;
&lt;p&gt;服务端双写降低了业务方升级的不确定性，却让平台承担更高性能风险；保留旧链路提供了回退能力，也增加了维护负担。方案并没有消除代价，只是选择了更适合当时组织条件的代价。&lt;/p&gt;
&lt;h2 id="旧系统迁移也是组织问题"&gt;旧系统迁移也是组织问题&lt;/h2&gt;
&lt;p&gt;技术上更整洁的方案，不一定最容易在组织中完成。&lt;/p&gt;
&lt;p&gt;当接入方数量很多、维护状态不一时，要求所有团队在同一时间配合升级，本身就是很高的协调成本。把兼容集中到平台端，有时比设计一套完美的新协议更现实。&lt;/p&gt;
&lt;p&gt;这次迁移让我更清楚地认识到，旧系统改造不仅要考虑最终架构，还要设计从现在到最终状态之间的路径。迁移方案的质量，往往体现在这段中间状态是否可控。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
一个长周期项目是怎样失控的</title><link>https://bug1024.com/posts/how-long-projects-lose-control/</link><guid isPermaLink="true">https://bug1024.com/posts/how-long-projects-lose-control/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我负责过一个横跨数月的项目。&lt;/p&gt;
&lt;p&gt;它涉及多个业务模块，也依赖几个外部系统。最初排期时，每一部分看起来都能估算，任务也按照模块完成拆分。但项目真正推进以后，需求多次调整，依赖方的节奏并不一致，联调阶段又出现了此前没有暴露的问题。&lt;/p&gt;
&lt;p&gt;长周期项目很少在某一天突然失控。更多时候，每周都有进展，整体却在逐渐偏离原来的计划。&lt;/p&gt;
&lt;h2 id="局部任务完成掩盖了关键路径"&gt;局部任务完成掩盖了关键路径&lt;/h2&gt;
&lt;p&gt;项目拆分以后，进度通常按照任务完成情况统计。完成数量持续增加，会让人觉得项目仍在正常向前。&lt;/p&gt;
&lt;p&gt;但并非所有任务同等重要。有些工作即使延期，也不会阻塞其他部分；有些关键接口只要没有确定，多个团队都会等待。若只看整体完成比例，很容易忽略真正决定交付时间的关键路径。&lt;/p&gt;
&lt;p&gt;负责人需要持续确认：当前哪个不确定因素最可能影响最终上线，谁在等待谁，以及这个等待是否已经超出原计划。&lt;/p&gt;
&lt;h2 id="需求变化并不只是增加工作量"&gt;需求变化并不只是增加工作量&lt;/h2&gt;
&lt;p&gt;长项目很难完全避免需求调整。&lt;/p&gt;
&lt;p&gt;问题在于，一项变化可能影响已经完成的设计、测试范围和上下游约定。只给新增内容补充工时，往往低估了重新理解和重新对齐的成本。&lt;/p&gt;
&lt;p&gt;变化发生以后，需要重新检查项目结构，而不是简单把新任务放进原计划。原来的拆分是否仍然成立，已经完成的部分能否保留，关键路径有没有改变，都应该重新确认。&lt;/p&gt;
&lt;p&gt;有时最合理的选择不是继续追赶原排期，而是缩小首次交付范围。&lt;/p&gt;
&lt;h2 id="高频同步也可能没有同步到问题"&gt;高频同步也可能没有同步到问题&lt;/h2&gt;
&lt;p&gt;长周期项目通常会安排定期进度会议。每个人汇报已经完成什么、接下来做什么，会议看起来很完整。&lt;/p&gt;
&lt;p&gt;但如果所有人只汇报自己的任务，跨团队分歧可能仍然没有被看见。一个团队说“接口已经完成”，另一个团队理解的格式和时机却可能不同。直到联调开始，双方才发现各自完成的是不同答案。&lt;/p&gt;
&lt;p&gt;有效同步不只是报告进度，还要确认依赖条件是否成立。尤其是跨系统数据、状态和上线顺序，需要用同一条真实流程串起来检查。&lt;/p&gt;
&lt;h2 id="风险会在等待中变得普通"&gt;风险会在等待中变得普通&lt;/h2&gt;
&lt;p&gt;项目早期出现的风险，如果连续几周没有解决，团队容易逐渐习惯它的存在。&lt;/p&gt;
&lt;p&gt;“依赖方还没确定”“数据量以后再确认”“配置上线方式待讨论”，这些事项每周都在列表里，却没有明确决策时间。它们看起来没有继续恶化，实际上不断压缩后续处理空间。&lt;/p&gt;
&lt;p&gt;风险项需要责任人和最晚决策点。超过这个时间，就应该调整范围、方案或排期，而不是继续等待一个理想答案。&lt;/p&gt;
&lt;h2 id="失控以后首先恢复判断"&gt;失控以后首先恢复判断&lt;/h2&gt;
&lt;p&gt;项目偏离时，最直观的反应是加快速度、增加会议或要求加班。这些动作可能提高局部产出，却不能自动修复错误依赖和变化后的计划。&lt;/p&gt;
&lt;p&gt;更重要的是重新建立项目基线：当前必须交付什么，哪些已经完成且有效，剩余关键路径是什么，哪些风险需要更高层级决定。&lt;/p&gt;
&lt;p&gt;长周期项目需要的不是从开始到结束始终遵守同一份计划，而是在条件变化以后，仍然能够形成一份可信的新计划。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
在 AI 时代，重新做一个自己的博客</title><link>https://bug1024.com/posts/why-i-restart/</link><guid isPermaLink="true">https://bug1024.com/posts/why-i-restart/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;上一篇博客停在 2020 年 6 月。六年后重新打开它，我没有直接发布一篇新文章，而是先把整个博客重新做了一遍。&lt;/p&gt;
&lt;p&gt;我重构了博客框架，重新整理文章和专题，调整了首页与文章页的关系，也为网站设计了一套统一的视觉风格。沿用了十多年的 &lt;code&gt;BUG1024&lt;/code&gt;，这次也终于有了专属标志。&lt;/p&gt;
&lt;p&gt;更重要的是，完成这些事情的方式和过去已经不同。AI 参与了这次重启的几乎每一个环节。&lt;/p&gt;
&lt;h2 id="不是简单恢复更新"&gt;不是简单恢复更新&lt;/h2&gt;
&lt;p&gt;这个博客最活跃的年份是 2017 年。那时写的大多是技术文章：遇到一个问题，查阅资料，完成实践，再把过程整理出来。&lt;/p&gt;
&lt;p&gt;后来工作中的问题不再局限于某个技术点。项目、业务、协作和组织逐渐交织在一起，生活里也有许多变化值得记录。能写的内容更多了，真正写成文章的反而越来越少。&lt;/p&gt;
&lt;p&gt;博客停下来，不是因为没有想法，而是整理材料、组织结构、修改文字的成本很高。工作一忙，写作总会排到后面。那些想法最后散落在日记、旧博客、学习笔记、工作记录和记忆里。&lt;/p&gt;
&lt;p&gt;所以这次重新开始，我不想只恢复更新频率。我更想重新确定这个博客写什么、怎样组织，以及它应该以什么样子存在。&lt;/p&gt;
&lt;h2 id="ai-让很多事情变得可能"&gt;AI 让很多事情变得可能&lt;/h2&gt;
&lt;p&gt;我对 AI 有一种很具体的感激。&lt;/p&gt;
&lt;p&gt;过去想做一件事，往往要先判断自己缺少什么：不会设计，不熟悉某段前端代码，没有足够时间整理资料，或者很难独自完成所有细节。许多想法并没有被否定，只是被放进了“以后有空再做”的列表。&lt;/p&gt;
&lt;p&gt;AI 改变了这件事。&lt;/p&gt;
&lt;p&gt;我可以先描述想要的效果，让它分析现有代码、提出方案、完成实现，再根据实际页面继续修改。我也可以把零散的文字、真实案例和个人感受交给它，让它整理关系、搭建结构，然后逐段判断哪些准确，哪些失真。&lt;/p&gt;
&lt;p&gt;AI Coding 也像是唤醒了我沉寂一段时间的技术热情。如果一定要概括，大概算是一场“第二春”。它让我想起当年那个会通宵折腾 Vim 配置、顺着一个问题研究开源软件的自己。&lt;/p&gt;
&lt;p&gt;我怀念的不是通宵，而是那种因为好奇而持续投入，想到一个东西就想亲手验证的状态。工作多年以后，这种热情曾被项目和日常消耗遮住，现在又以另一种方式回来了。&lt;/p&gt;
&lt;p&gt;某种意义上，有了 AI 以后，我需要做的第一件事只是发挥想象力：先想清楚一件事可以是什么样，而不是先被自己暂时不会什么劝退。&lt;/p&gt;
&lt;p&gt;想象力当然不等于结果。方向是否合适，事实是否准确，设计是否符合审美，文字是否真正表达了我的意思，仍然需要我作出判断。但 AI 缩短了想法与实现之间的距离，也让一个人能够完成过去需要多种能力配合的事情。&lt;/p&gt;
&lt;h2 id="从框架到视觉重新做一次"&gt;从框架到视觉，重新做一次&lt;/h2&gt;
&lt;p&gt;这次重启不是简单更换一个博客主题。&lt;/p&gt;
&lt;p&gt;我重新统一了文章地址，建立专题阅读路径，让历史文章保留时间顺序，也让同一批内容可以按照经历和问题重新组织。首页不再只是最新文章列表，而是成为整个博客的阅读入口。&lt;/p&gt;
&lt;p&gt;视觉上，我去掉了与正文关系不大的题图，重新设计首页、专题、关于页和文章详情页。墨色、雾白和钢蓝构成了现在的主要色彩，留白、字号和分隔都经过多轮调整。它不需要看起来热闹，但应该让人知道这些地方经过思考。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BUG1024&lt;/code&gt; 这个代号和域名已经伴随我十多年。我没有给它附加复杂的图腾，只在完整字标的 &lt;code&gt;G&lt;/code&gt; 上留下一处异常：一枚从主体中错开的钢蓝色模块。第一眼仍然能读出 BUG1024，第二眼才会发现那个 Bug。&lt;/p&gt;
&lt;p&gt;它既是一个技术背景留下的名字，也提醒我，身处系统之中，仍然要保留自己的判断。&lt;/p&gt;
&lt;p&gt;这些工作由我提出方向、不断取舍，AI 帮助我分析、设计和实现。很多细节并不是一次生成出来的，而是在一次次“还不对”“太空了”“有些刻意”之后逐渐形成。&lt;/p&gt;
&lt;h2 id="ai-也改变了写作"&gt;AI 也改变了写作&lt;/h2&gt;
&lt;p&gt;博客接下来也会使用同样的协作方式。&lt;/p&gt;
&lt;p&gt;我提供题目、背景、事实、真实案例、个人判断和感受，AI 帮助整理资料、组织结构并形成初稿。我再确认事实是否准确、观点是否成立，删掉那些听起来正确、实际上并不属于我的句子。&lt;/p&gt;
&lt;p&gt;AI 可以根据我过去的文章、日记片段、对话和修改意见逐渐接近我的表达习惯，但它不能替我经历，也不能替我决定一件事为什么值得写。&lt;/p&gt;
&lt;p&gt;前段时间，我已经用这种方式整理了多年来散落的资料。早年的技术实践、项目复盘、团队协作和职业阶段中的思考，原本只是互不相连的片段；经过筛选、补充背景和重新组织以后，它们成为了可以重新阅读的文章。&lt;/p&gt;
&lt;p&gt;这些文章由 AI 协助完成，却不是无中生有。生成文字变得容易以后，真正重要的反而是能否提供真实材料，能否识别空洞而流畅的表达，以及是否愿意对最后留下的内容负责。&lt;/p&gt;
&lt;h2 id="这里接下来会写什么"&gt;这里接下来会写什么&lt;/h2&gt;
&lt;p&gt;这里仍然会写少量技术，但不会再以技术教程为主。相比一个框架怎样使用，我现在更关心技术如何进入真实业务，一套系统为什么会变成现在这样，人在协作与组织中如何作出判断。&lt;/p&gt;
&lt;p&gt;也会写生活、城市、家庭、时间和个人变化。我从 2004 年开始大量写日记，其中有些文字只适合留给自己，也有一些值得在多年后重新整理。过去的看法未必与今天一致，这种差异本身也是成长的一部分。&lt;/p&gt;
&lt;p&gt;我不会为了更新频率凭空寻找题目。文章可以来自最近发生的事，也可以来自过去留下的资料，但应该有真实出处，也应该保留我自己的判断。&lt;/p&gt;
&lt;h2 id="重新开始"&gt;重新开始&lt;/h2&gt;
&lt;p&gt;一个沿用了十多年的名字，一批散落多年的文字，以及一些曾经因为时间和能力边界而搁置的想法，现在重新组成了一个完整的博客。&lt;/p&gt;
&lt;p&gt;我感谢 AI，不只是因为它提高了效率。更重要的是，它让很多原本只能停留在想象中的事情，有了被做出来的可能。它没有替我生活，也没有替我思考，却让我能够把经历过的事、形成的判断，以及一些不必急着得出结论的感受，整理成可以长期留下的东西。&lt;/p&gt;
&lt;p&gt;写到这里，已经是凌晨两点。我又像很多年前那样坐在电脑前，反复改代码、看页面，只为了让这个博客按自己想要的样子重新上线。不同的是，这一次身边多了 AI，而那种想把一件事做完的兴奋，并没有变。&lt;/p&gt;
&lt;p&gt;这不是回到过去，而是用现在能够掌握的方式，把过去留下的材料和以后发生的事情继续写下去。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
做得多，不一定产出更多</title><link>https://bug1024.com/posts/doing-more-is-not-producing-more/</link><guid isPermaLink="true">https://bug1024.com/posts/doing-more-is-not-producing-more/</guid><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><description>
&lt;p&gt;我一直不太喜欢无所事事。&lt;/p&gt;
&lt;p&gt;早起、学习、锻炼、制定计划，这些习惯很早就出现在日记里。工作以后，事情变得更多，空闲时间也很容易被新的任务填满。&lt;/p&gt;
&lt;p&gt;做事会带来一种直接的确定感：时间没有被浪费，自己仍在向前。只是后来我逐渐发现，行动很多与产出有效并不是同一件事。&lt;/p&gt;
&lt;h2 id="忙碌能够缓解不确定"&gt;忙碌能够缓解不确定&lt;/h2&gt;
&lt;p&gt;面对一个没有想清楚的问题，最容易做的是先找一件具体的事。&lt;/p&gt;
&lt;p&gt;读一本书，学习一个工具，整理一份计划，或者解决几个难度不高的任务。每完成一项，都会得到清楚反馈。相比继续面对一个边界模糊、短期看不到结果的问题，这些行动让人更踏实。&lt;/p&gt;
&lt;p&gt;它们并非没有价值。问题在于，有时我选择它们，不是因为它们最重要，而是因为它们最容易开始和结束。&lt;/p&gt;
&lt;p&gt;忙碌由此可能成为一种回避。每天都很充实，真正需要作出的决定却一直没有推进。&lt;/p&gt;
&lt;h2 id="学习也可能是一种延迟"&gt;学习也可能是一种延迟&lt;/h2&gt;
&lt;p&gt;技术工作鼓励持续学习，这很容易让学习天然显得正确。&lt;/p&gt;
&lt;p&gt;但学习什么、为什么学，仍然需要判断。过去我保存过大量文章，建立过很多专题。其中一部分确实帮助我解决问题，另一部分只是扩大了“以后可能会用到”的资料库。&lt;/p&gt;
&lt;p&gt;如果一项学习既没有回应当前问题，也没有进入长期关注的方向，它可能只是比娱乐更容易获得认可的消遣。&lt;/p&gt;
&lt;p&gt;学习不必每次都追求立即回报。只是需要诚实地区分：这是出于兴趣的阅读，还是为了解决问题；是在积累长期能力，还是因为不知道下一步做什么。&lt;/p&gt;
&lt;h2 id="工作量也不能自动转换成价值"&gt;工作量也不能自动转换成价值&lt;/h2&gt;
&lt;p&gt;工作中同样存在这种差异。&lt;/p&gt;
&lt;p&gt;处理很多需求、参加很多会议、写很多代码，都可以证明投入。真正的产出却取决于问题是否解决，协作是否改善，系统是否因此更稳定。&lt;/p&gt;
&lt;p&gt;有些高价值工作并不忙碌。例如提前澄清一个错误需求，可能避免几周开发；决定不建设一套暂时用不到的平台，也可能节省长期维护成本。这些结果很难通过任务数量体现。&lt;/p&gt;
&lt;p&gt;反过来，不断救火看起来贡献很大，也可能说明相同问题一直没有得到治理。&lt;/p&gt;
&lt;h2 id="停止也是行动的一部分"&gt;停止也是行动的一部分&lt;/h2&gt;
&lt;p&gt;我过去更擅长开始和坚持，不太擅长主动放弃。&lt;/p&gt;
&lt;p&gt;一个项目只要已经投入时间，就会希望继续完成；一套计划即使不再合适，也容易因为中断显得可惜。但继续投入并不能让原来的决定自动变得正确。&lt;/p&gt;
&lt;p&gt;定期清理没有进展、价值已经变化或长期消耗注意力的事情，是另一种必要选择。停止之后，最好说明为什么停止，避免下一次在相似条件下重复开始。&lt;/p&gt;
&lt;h2 id="重新判断一天是否充实"&gt;重新判断一天是否充实&lt;/h2&gt;
&lt;p&gt;现在我仍然喜欢有安排的生活，也不会因此否定自律和勤奋。&lt;/p&gt;
&lt;p&gt;只是衡量一天时，我想少看完成了多少项，多看最重要的问题是否发生了实际变化。有时答案是一段完成的工作，有时只是一个更清楚的决定，也可能是确认一件事暂时不做。&lt;/p&gt;
&lt;p&gt;行动能够对抗停滞，却不能代替方向。做得更多以前，仍然需要先知道自己试图改变什么。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%94%9F%E6%B4%BB%E4%B8%8E%E6%97%B6%E9%97%B4/">生活与时间</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
理解OkHttp的连接复用、并发与超时</title><link>https://bug1024.com/posts/okhttp/</link><guid isPermaLink="true">https://bug1024.com/posts/okhttp/</guid><pubDate>Sat, 06 Jun 2020 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2020年，起因是一次连接没有复用的问题。2026年重新整理时删除了旧版本源码和官网示例，保留仍然有效的判断方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;HTTP客户端的使用方式很简单，运行行为却涉及DNS、连接建立、TLS、连接池、并发队列、超时和重试。只看一次请求是否成功，很难判断客户端在压力下会发生什么。&lt;/p&gt;
&lt;p&gt;我当时遇到的问题是：明明配置了连接池，请求之间却仍然反复建立连接。最后发现服务端响应明确要求关闭连接。这个细节让我意识到，连接复用不是客户端单方面的配置结果，而是协议、响应是否完整消费以及连接状态共同决定的。&lt;/p&gt;
&lt;h2 id="复用一个客户端"&gt;复用一个客户端&lt;/h2&gt;
&lt;p&gt;OkHttpClient内部持有连接池和异步调用调度器。为每次请求创建新客户端，会拆散这些可以共享的资源，连接复用也就失去了意义。&lt;/p&gt;
&lt;p&gt;通常应在应用范围内复用一个客户端；某次调用需要不同配置时，可以从共享客户端使用&lt;code&gt;newBuilder()&lt;/code&gt;派生。派生客户端能够共享连接池、调度器和已有配置，而不必重新建立一套网络资源。&lt;/p&gt;
&lt;h2 id="连接什么时候能够复用"&gt;连接什么时候能够复用&lt;/h2&gt;
&lt;p&gt;建立TCP和TLS连接需要额外往返，连接池会尽量复用符合条件的现有连接。HTTP/2还可以让同一连接承载多个并发流。&lt;/p&gt;
&lt;p&gt;但连接复用有前提：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;响应体被正确读取或关闭，连接才能及时归还；&lt;/li&gt;
&lt;li&gt;服务端或代理没有要求关闭连接；&lt;/li&gt;
&lt;li&gt;地址、代理、TLS配置等条件允许共享；&lt;/li&gt;
&lt;li&gt;连接仍然健康，没有被对端或网络设备回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此排查“为什么没有复用”时，不能只看&lt;code&gt;ConnectionPool&lt;/code&gt;参数。更有效的方法是通过&lt;code&gt;EventListener&lt;/code&gt;观察DNS、连接建立、连接获取和释放事件，确认请求到底在哪个阶段创建了新连接。&lt;/p&gt;
&lt;h2 id="dispatcher管理的是异步调用"&gt;Dispatcher管理的是异步调用&lt;/h2&gt;
&lt;p&gt;异步请求通过Dispatcher进入执行器。Dispatcher限制同时运行的调用总数和单个主机的并发数，超过限制的调用进入队列。&lt;/p&gt;
&lt;p&gt;这些限制主要约束异步调用，不等于整个系统的完整流量保护。同步调用由调用线程直接执行；即使客户端并发受控，下游服务、线程池和业务重试仍然可能放大流量。&lt;/p&gt;
&lt;p&gt;自定义执行器时也不能只把最大线程数调大。需要同时观察队列、单主机并发、请求耗时和下游容量。&lt;/p&gt;
&lt;h2 id="四类超时"&gt;四类超时&lt;/h2&gt;
&lt;p&gt;OkHttp提供几个容易混淆的超时维度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;connectTimeout&lt;/code&gt;：建立到目标地址连接的时间限制；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readTimeout&lt;/code&gt;：一次读取操作等待数据的时间限制；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeTimeout&lt;/code&gt;：一次写入操作的时间限制；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callTimeout&lt;/code&gt;：一次完整调用从开始到结束的总时间限制，包括连接、写入、服务端处理、读取以及可能发生的重试和跳转。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们解决的是不同问题。只设置读取超时，不能约束一次调用经历多个阶段后的总耗时；把所有超时设成同一个数字，也不代表符合实际链路。&lt;/p&gt;
&lt;p&gt;参数应来自调用目标、接口SLA和失败后的处理方式。对于不可安全重复的写操作，还要谨慎评估自动重试可能造成的副作用。&lt;/p&gt;
&lt;h2 id="从结果转向过程"&gt;从结果转向过程&lt;/h2&gt;
&lt;p&gt;网络调用的不同问题经常被统一描述为“请求超时”。DNS慢、连接失败、TLS慢、连接池没有命中、服务端迟迟不返回数据，最终都可能表现为调用失败。&lt;/p&gt;
&lt;p&gt;理解OkHttp时，与其记住某个版本的默认参数，不如把一次调用拆成可观察的阶段，再判断连接、并发和超时分别在控制什么。这样得到的客户端配置，也更容易与实际链路对应。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://square.github.io/okhttp/features/events/"&gt;OkHttp Events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://square.github.io/okhttp/recipes/"&gt;OkHttp Recipes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://square.github.io/okhttp/contribute/concurrency/"&gt;OkHttp Concurrency&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
2017年总结</title><link>https://bug1024.com/posts/2017summary/</link><guid isPermaLink="true">https://bug1024.com/posts/2017summary/</guid><pubDate>Mon, 01 Jan 2018 00:00:00 +0800</pubDate><description>
&lt;p&gt;转眼又是一年，也是来到北京后的第一年。与往年一样，做一次相对完整的记录。&lt;/p&gt;
&lt;h2 id="这一年完成的事情"&gt;这一年完成的事情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;从PHP转向Java，参与较大流量业务系统的重构，对分布式服务有了更具体的认识；&lt;/li&gt;
&lt;li&gt;开始学习PHP内核，能够完成简单的扩展开发；&lt;/li&gt;
&lt;li&gt;重新学习MySQL与Redis，对数据持久化和缓存应用有了进一步理解；&lt;/li&gt;
&lt;li&gt;GitHub连续提交从1月1日持续到8月5日，共217天；&lt;/li&gt;
&lt;li&gt;搭建独立博客，全年写了57篇文章；&lt;/li&gt;
&lt;li&gt;连续跑步100天，基本保持每周打一次篮球，体重变化不大；&lt;/li&gt;
&lt;li&gt;断续学习英语，新增了一些词汇。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="编程"&gt;编程&lt;/h2&gt;
&lt;p&gt;这一年职业上最明显的变化，是转向Java开发。我以前觉得Java过于笨重，真正使用以后，开始理解它在大型团队和长期系统中的一些取舍。&lt;/p&gt;
&lt;p&gt;参与系统重构的过程，让我接触了容量评估、风险识别、核心指标和线上问题定位。但这些认识仍处在建立阶段，尤其是网络、JVM和多线程方面，离熟练处理复杂问题还有距离。&lt;/p&gt;
&lt;p&gt;技术分享做得也不够。写了不少博客，但很多仍是学习笔记，缺少来自实践的问题和验证。接下来希望提升的不只是文章数量，还包括文档、作图和把问题讲清楚的能力。&lt;/p&gt;
&lt;h2 id="生活"&gt;生活&lt;/h2&gt;
&lt;p&gt;运动量保持在中等水平，体重没有明显变化。篮球外线投射有所改善，但突破和跑跳能力比以前弱了一些。白头发继续增多，这件小事仍会影响我对年龄的感受。&lt;/p&gt;
&lt;p&gt;这一年去电影院看了6部电影，在网上看了不少武侠片，主要是狄龙的作品；还看了《济公游记》和《七龙珠》。周末重新开始做饭，谈不上精细，但能够解决日常需要。&lt;/p&gt;
&lt;p&gt;新的一年希望继续运动，也适当减少影视内容，重新回到纸质阅读。作息方面，目标不是给自己设一条不能违反的时间线，而是尽量减少没有必要的熬夜。&lt;/p&gt;
&lt;h2 id="英语"&gt;英语&lt;/h2&gt;
&lt;p&gt;过去几年都制定过英语计划，但执行始终不稳定。2017年的学习仍以背单词为主，新增词汇有限，听说和阅读没有形成持续习惯。&lt;/p&gt;
&lt;p&gt;2018年准备把目标拆得更实际一些：保持阅读和听力输入，逐步增加词汇，并寻找能够开口使用英语的场景。与其再次列出过高的年度指标，不如先把学习变成可以长期维持的日常安排。&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这一年的进步没有完全达到预期，一部分原因是计划过多，也高估了自己能够同时投入的精力。&lt;/p&gt;
&lt;p&gt;接近三十岁时，我仍然希望生活能够按照一定秩序向前，但也开始接受，计划只是方向，不可能控制所有结果。能做的是持续调整，把时间留给真正重要的事情。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
业务开发如何保持技术成长</title><link>https://bug1024.com/posts/grow/</link><guid isPermaLink="true">https://bug1024.com/posts/grow/</guid><pubDate>Mon, 13 Nov 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年重新整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;工作三年多时，我开始反复听到一种说法：业务代码没有技术含量。它说中了一部分事实——重复实现相似功能，确实不会自动带来能力增长；但问题未必在业务本身，也可能在于我们只完成了需求，没有继续追问系统为什么这样运行。&lt;/p&gt;
&lt;p&gt;当时我把技术学习归纳成三个动作：Learning、Trying、Teaching。多年后再看，这个结构仍然有效，只是它们不应该彼此分离。&lt;/p&gt;
&lt;h2 id="系统学习"&gt;系统学习&lt;/h2&gt;
&lt;p&gt;搜索和短文章适合解决一个明确问题，却很难独立建立完整认识。操作系统、网络、数据库和运行时这类基础内容，仍然需要相对系统地学习。&lt;/p&gt;
&lt;p&gt;系统学习不等于从头背完一本书。更实际的方式是先建立问题地图：核心概念之间是什么关系，哪些机制会影响当前工作，哪些地方自己还无法解释。之后再用文档、书籍和源码补足细节。&lt;/p&gt;
&lt;p&gt;学习的结果不应只是收藏更多资料，而是能用自己的语言说明一个机制及其适用边界。&lt;/p&gt;
&lt;h2 id="动手验证"&gt;动手验证&lt;/h2&gt;
&lt;p&gt;很多技术概念在实际运行以后，更容易暴露理解上的空白。&lt;/p&gt;
&lt;p&gt;例如学习JVM垃圾回收，可以写一个持续分配内存的小程序，调整参数，再观察堆、停顿和回收日志；学习Reactor，可以先实现一个最小事件循环，再对照成熟框架如何处理线程和连接；学习数据库，可以从慢查询、索引和实际执行计划出发，而不是只记住几条优化原则。&lt;/p&gt;
&lt;p&gt;验证不一定需要完整项目。一个能够证伪自己判断的最小实验，往往比照着教程搭建一套复杂环境更有效。&lt;/p&gt;
&lt;h2 id="把问题讲清楚"&gt;把问题讲清楚&lt;/h2&gt;
&lt;p&gt;当一个人需要把问题讲给别人听时，会被迫处理很多原本可以含糊过去的地方：概念的边界是什么，结论依赖什么条件，别人为什么应该相信。&lt;/p&gt;
&lt;p&gt;这里的“讲”不一定是公开演讲，也可以是一篇文档、一次设计评审或给同事解释故障原因。反馈会暴露遗漏，反过来推动下一轮学习和验证。&lt;/p&gt;
&lt;h2 id="回到业务现场"&gt;回到业务现场&lt;/h2&gt;
&lt;p&gt;技术成长不能长期停留在模拟环境。更复杂的部分通常来自业务约束：数据不能停、上下游不可控、团队需要协作、方案还要考虑成本。&lt;/p&gt;
&lt;p&gt;因此更完整的循环应当是：从工作中发现具体问题，系统学习相关机制，用最小实验验证判断，再把结论写清楚并带回实际系统。业务不是技术成长的障碍，它提供了问题和约束；是否继续深入，取决于我们有没有停在“功能已经完成”这一步。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
突然想到</title><link>https://bug1024.com/posts/thought/</link><guid isPermaLink="true">https://bug1024.com/posts/thought/</guid><pubDate>Tue, 24 Oct 2017 00:00:00 +0800</pubDate><description>
&lt;p&gt;毕业参加工作已经三年半。一路走来没有经历太大的波折，因此每当听到身边人讲起各自的困难，我都会更加意识到，自己过去得到过不少支持和运气。&lt;/p&gt;
&lt;p&gt;我仍然喜欢程序员这份工作。它要求持续学习，而学习本身会带来一种相对踏实的满足感。&lt;/p&gt;
&lt;p&gt;人与人的条件和节奏并不相同。承认差异，不急着用别人的进度评价自己，可以减少很多没有必要的压力。&lt;/p&gt;
&lt;p&gt;我喜欢规律的生活，也希望保持自律，但计划被打乱时仍然需要调整。规律提供秩序，适应变化则让这种秩序不至于变成负担。&lt;/p&gt;
&lt;p&gt;过去大半年有两件事没有做好：早睡和学习英语。偶尔会想去国外生活一段时间，看看离开熟悉环境之后，自己会如何应对。&lt;/p&gt;
&lt;p&gt;最近总梦见自己在逃亡，沿途经过的却都是熟悉的旧景。醒来以后才发现，自己比想象中更容易怀念过去。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%94%9F%E6%B4%BB%E4%B8%8E%E6%97%B6%E9%97%B4/">生活与时间</category></item><item><title>
工作中的时间管理：先减少切换</title><link>https://bug1024.com/posts/time-manage/</link><guid isPermaLink="true">https://bug1024.com/posts/time-manage/</guid><pubDate>Wed, 05 Jul 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年重新整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我过去理解的时间管理，主要是把一天划分成不同区间：什么时候编码，什么时候回复消息，什么时候开会。这样做有一定作用，但后来越来越觉得，频繁切换任务同样会消耗大量精力。&lt;/p&gt;
&lt;p&gt;消息刚出现就回复，临时需求马上开始做，做到一半又去参加会议。一天看起来一直很忙，需要连续思考的事情却可能没有完成。&lt;/p&gt;
&lt;h2 id="给临时事项一个入口"&gt;给临时事项一个入口&lt;/h2&gt;
&lt;p&gt;临时出现的事情不一定要立即处理，但最好先被记录下来。&lt;/p&gt;
&lt;p&gt;我会先确认三件事：它是否真的紧急，最晚什么时候需要结果，不处理会影响谁。能够回答这三个问题，才有可能把它放进已有计划，而不是让声音最大的人自动获得最高优先级。&lt;/p&gt;
&lt;p&gt;记录不是为了建立越来越长的清单，而是为了让大脑不必持续记住“还有一件事没做”。没有负责人、完成条件和时间边界的事项，也不算一个可以执行的任务。&lt;/p&gt;
&lt;h2 id="保护连续工作的时间"&gt;保护连续工作的时间&lt;/h2&gt;
&lt;p&gt;编码、方案设计和问题排查都需要上下文。每次切换之后，重新恢复上下文也要付出时间。&lt;/p&gt;
&lt;p&gt;因此我更愿意把需要集中思考的工作放在相对完整的时间段，把回复消息、处理零散事项集中到几个固定节点。具体是上午还是下午并不重要，重要的是根据自己的状态和团队协作节奏，保住几段不被随意切碎的时间。&lt;/p&gt;
&lt;p&gt;如果岗位本身要求快速响应，可以明确值守和深度工作的轮换，减少团队成员长期处于随时被打断的状态。&lt;/p&gt;
&lt;h2 id="沟通方式由问题决定"&gt;沟通方式由问题决定&lt;/h2&gt;
&lt;p&gt;以前我给沟通方式排过固定优先级：当面、电话、即时消息、邮件。现在看，这个排序过于简单。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要快速澄清、分歧较大的问题，实时沟通通常更有效；&lt;/li&gt;
&lt;li&gt;需要精确记录、多人确认的结论，应当落到文档或邮件；&lt;/li&gt;
&lt;li&gt;简单状态同步，用即时消息即可；&lt;/li&gt;
&lt;li&gt;复杂问题先异步提供背景，再开会讨论，往往比临时拉人更节省时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键不是使用哪种工具，而是让参与者拥有足够背景，并在沟通结束后留下明确结论。&lt;/p&gt;
&lt;h2 id="给计划留下余量"&gt;给计划留下余量&lt;/h2&gt;
&lt;p&gt;如果每天都按百分之百排满，一次临时变化就可能打乱整套计划。工作中难免出现线上问题、需求变化和跨团队协作，余量不是浪费，而是应对变化的空间。&lt;/p&gt;
&lt;p&gt;时间管理最终不是把每一分钟都变成产出，而是让重要的事情获得足够连续的注意力，也让临时事情有可以进入和退出的秩序。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
2017上半年总结</title><link>https://bug1024.com/posts/2017-middle-summary/</link><guid isPermaLink="true">https://bug1024.com/posts/2017-middle-summary/</guid><pubDate>Wed, 28 Jun 2017 00:00:00 +0800</pubDate><description>
&lt;p&gt;转眼半年过去，比想象中快得多。这也是我来到北京后的第一个半年。&lt;/p&gt;
&lt;h2 id="编程"&gt;编程&lt;/h2&gt;
&lt;p&gt;我过去不太喜欢Java，觉得它比PHP笨重。没想到换了工作之后，主要使用的恰恰是Java。&lt;/p&gt;
&lt;p&gt;真正开始使用后，我对面向对象编程有了更完整的理解，写代码时也更加重视边界和约束。不过当时对Java运行原理的认识仍然有限，排查问题也容易停在表层。&lt;/p&gt;
&lt;p&gt;操作系统方面投入的时间不够，下半年需要补充。GitHub连续提交已经保持半年，但提交次数本身并没有明显改善能力。相比维持记录，我更需要做一些能够验证理解的项目或练习。&lt;/p&gt;
&lt;h2 id="运动"&gt;运动&lt;/h2&gt;
&lt;p&gt;这半年基本保持了运动习惯，体重也相对稳定。下班较晚时，我仍然会尽量留一点时间活动身体。&lt;/p&gt;
&lt;p&gt;膝盖和弹跳不如以前，让我开始意识到运动不能只看次数，也要关注恢复和方式。保持体能仍然重要，但没有必要用超出身体状态的目标证明意志力。&lt;/p&gt;
&lt;h2 id="朋友与独处"&gt;朋友与独处&lt;/h2&gt;
&lt;p&gt;我不擅长主动社交，很多朋友都是打球时认识的。随着曾经的朋友陆续建立家庭，联系自然比以前少了一些。&lt;/p&gt;
&lt;p&gt;我习惯独处，因此没有太多不适，但也逐渐明白，能够独处和维持关系并不冲突。联系不必频繁，重要的是彼此仍然知道对方过得怎样。&lt;/p&gt;
&lt;h2 id="北京"&gt;北京&lt;/h2&gt;
&lt;p&gt;来北京算是完成了一个想法。真正生活半年后发现，日常并没有想象中那么戏剧化，仍然是工作、学习、运动和休息。&lt;/p&gt;
&lt;p&gt;我想买辆车，去更远的地方看看，不过在北京实现这件事并不容易。无论如何，生活应该保留一些工作之外的内容。&lt;/p&gt;
&lt;p&gt;北漂才刚开始。接下来继续适应，也继续观察这座城市会给自己带来什么变化。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
PHP为什么需要ZTS</title><link>https://bug1024.com/posts/php-thread-safe/</link><guid isPermaLink="true">https://bug1024.com/posts/php-thread-safe/</guid><pubDate>Wed, 07 Jun 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年重新整理。早期文章关注TSRM的具体实现，现在更值得保留的是它要解决的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PHP应用最常见的运行方式是FPM：多个worker进程分别处理请求。进程拥有独立地址空间，因此一个worker修改自身的全局状态，不会直接改到另一个worker。&lt;/p&gt;
&lt;p&gt;但PHP也可以被构建在多线程环境中。线程共享同一进程的地址空间，如果执行器、编译器或扩展把可变状态放在普通全局变量里，不同线程就可能互相覆盖数据。ZTS（Zend Thread Safety）就是为这种运行模型准备的。&lt;/p&gt;
&lt;h2 id="zts解决什么"&gt;ZTS解决什么&lt;/h2&gt;
&lt;p&gt;它的基本思路并不神秘：不能安全共享的全局状态，不再让所有线程访问同一份，而是为不同线程提供各自的存储，再通过线程上下文取得当前线程对应的数据。&lt;/p&gt;
&lt;p&gt;早期PHP内核通过TSRM（Thread Safe Resource Manager）管理这些资源。具体结构和访问宏随着PHP版本发生过变化，但目标没有变：让原本依赖进程级全局状态的代码，可以在多线程SAPI或嵌入场景中隔离运行。&lt;/p&gt;
&lt;p&gt;ZTS不是给业务代码自动加锁，也不能让任意扩展天然线程安全。扩展如果使用自己的全局资源、调用非线程安全的第三方库，仍然需要单独处理并发问题。&lt;/p&gt;
&lt;h2 id="什么时候需要"&gt;什么时候需要&lt;/h2&gt;
&lt;p&gt;是否启用ZTS取决于PHP运行环境，而不是应用是否“并发”。FPM通过多进程提供并发，通常不需要因为请求量增加就改成ZTS构建。&lt;/p&gt;
&lt;p&gt;通常需要考虑ZTS的场景包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PHP运行在多线程SAPI中；&lt;/li&gt;
&lt;li&gt;PHP被嵌入一个多线程宿主程序；&lt;/li&gt;
&lt;li&gt;使用明确要求ZTS的线程或并行扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ZTS是编译期选项，不能在已经安装好的PHP上临时打开。选择之前还需要确认扩展兼容性和运行方式，而不是把它当作通用性能开关。&lt;/p&gt;
&lt;h2 id="进程隔离与线程隔离"&gt;进程隔离与线程隔离&lt;/h2&gt;
&lt;p&gt;FPM的进程模型占用更多独立内存，但隔离边界直观，一个worker崩溃通常不会直接破坏其他worker的地址空间。线程模型共享资源更多，切换成本和内存结构不同，同时也要求运行时与扩展更严格地处理共享状态。&lt;/p&gt;
&lt;p&gt;理解ZTS的意义，不在于记住某个版本的内核结构，而在于先判断程序运行在什么并发模型里，再决定状态应当如何隔离。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/zh/configure.about.php"&gt;PHP核心编译选项&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
理解 PHP-FPM 的进程模型</title><link>https://bug1024.com/posts/php-fpm/</link><guid isPermaLink="true">https://bug1024.com/posts/php-fpm/</guid><pubDate>Sun, 04 Jun 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年根据PHP官方文档重新整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PHP-FPM是FastCGI进程管理器。以Nginx和PHP的常见组合为例，Nginx接收HTTP请求，将需要PHP处理的部分通过FastCGI交给FPM；FPM管理一组worker进程，每个worker在同一时刻处理一个请求。&lt;/p&gt;
&lt;p&gt;这个模型不复杂，但配置不当时很容易出现两种结果：worker太少，请求排队；worker太多，内存耗尽或引发更严重的资源竞争。&lt;/p&gt;
&lt;h2 id="master与worker"&gt;master与worker&lt;/h2&gt;
&lt;p&gt;FPM启动后由master进程管理pool和worker。master负责进程生命周期、配置和信号处理，worker负责执行具体的PHP请求。&lt;/p&gt;
&lt;p&gt;不同pool可以使用不同的用户、监听地址和进程参数，从而隔离不同应用。但pool增多并不会创造额外资源，所有worker最终仍然共享机器的CPU、内存和I/O能力。&lt;/p&gt;
&lt;h2 id="三种进程管理模式"&gt;三种进程管理模式&lt;/h2&gt;
&lt;p&gt;FPM通过&lt;code&gt;pm&lt;/code&gt;选择worker数量的管理方式。&lt;/p&gt;
&lt;h3 id="static"&gt;static&lt;/h3&gt;
&lt;p&gt;始终保持&lt;code&gt;pm.max_children&lt;/code&gt;个worker。数量固定、行为容易预估，适合流量稳定且资源预算明确的服务。&lt;/p&gt;
&lt;h3 id="dynamic"&gt;dynamic&lt;/h3&gt;
&lt;p&gt;启动时创建&lt;code&gt;pm.start_servers&lt;/code&gt;个worker，再根据空闲进程数量，在&lt;code&gt;pm.min_spare_servers&lt;/code&gt;、&lt;code&gt;pm.max_spare_servers&lt;/code&gt;和&lt;code&gt;pm.max_children&lt;/code&gt;约束下动态增减。&lt;/p&gt;
&lt;p&gt;它适合有波动但仍希望保留一定预热能力的场景。&lt;/p&gt;
&lt;h3 id="ondemand"&gt;ondemand&lt;/h3&gt;
&lt;p&gt;启动时不预先创建worker，请求到来时再生成，空闲时间超过&lt;code&gt;pm.process_idle_timeout&lt;/code&gt;后退出。它可以减少低频服务的常驻内存，但冷启动和突发流量下的进程创建成本需要纳入考虑。&lt;/p&gt;
&lt;h2 id="如何确定worker数量"&gt;如何确定worker数量&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pm.max_children&lt;/code&gt;限制可以同时处理的请求数。它不应从网上复制一个固定值，而应结合实际观测：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;测量真实业务worker的平均和高位内存占用；&lt;/li&gt;
&lt;li&gt;为操作系统、Nginx、日志和其他服务预留内存；&lt;/li&gt;
&lt;li&gt;用可分配给FPM的内存除以单个worker的高位占用，得到初步上限；&lt;/li&gt;
&lt;li&gt;再通过压测和线上延迟、队列、CPU与数据库连接情况校正。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;内存允许不等于下游能够承受。worker增加后，数据库连接、缓存请求和外部调用也会同时增加。&lt;/p&gt;
&lt;h2 id="比进程数更重要的观测"&gt;比进程数更重要的观测&lt;/h2&gt;
&lt;p&gt;实际运行中至少应该关注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;活跃、空闲和总worker数量；&lt;/li&gt;
&lt;li&gt;是否频繁达到&lt;code&gt;pm.max_children&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;请求排队和响应时间；&lt;/li&gt;
&lt;li&gt;慢请求及其调用栈；&lt;/li&gt;
&lt;li&gt;worker内存是否随请求持续增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pm.max_requests&lt;/code&gt;可以让worker处理一定请求后重启，对第三方库的内存泄漏具有缓解作用，但它不能替代定位泄漏原因。&lt;/p&gt;
&lt;p&gt;FPM调优的重点并不是找到一组“最佳参数”，而是让并发能力、内存预算和下游容量保持一致，并能在达到边界之前被观察到。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.php.net/manual/zh/install.fpm.configuration.php"&gt;PHP-FPM配置说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
从 select、poll 到 epoll：I/O复用解决了什么</title><link>https://bug1024.com/posts/io-multiplexing/</link><guid isPermaLink="true">https://bug1024.com/posts/io-multiplexing/</guid><pubDate>Wed, 10 May 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年重新核对并整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个线程如果只阻塞等待一个连接，模型很容易理解，但连接数上升后会带来大量线程及上下文切换。I/O复用提供了另一种方式：让一个调用同时等待多个文件描述符的就绪事件，再处理其中已经可以读写的部分。&lt;/p&gt;
&lt;p&gt;它解决的是“等待很多连接”的问题，不会自动解决业务处理过慢，也不意味着单线程可以完成所有工作。&lt;/p&gt;
&lt;h2 id="select"&gt;select&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt;通过多个&lt;code&gt;fd_set&lt;/code&gt;分别描述需要关注的可读、可写和异常事件。调用返回后，应用需要检查这些集合，找出已经就绪的文件描述符。&lt;/p&gt;
&lt;p&gt;它的优点是接口历史悠久、可移植性较好；限制也很明显：描述符集合存在容量约束，每次调用都要重新准备集合，返回后还要遍历检查。&lt;/p&gt;
&lt;h2 id="poll"&gt;poll&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt;使用&lt;code&gt;pollfd&lt;/code&gt;数组表达描述符和关注的事件，不再依赖&lt;code&gt;fd_set&lt;/code&gt;的固定表示方式，因此通常没有&lt;code&gt;select&lt;/code&gt;那样的描述符编号上限。&lt;/p&gt;
&lt;p&gt;但它仍然需要把关注集合交给内核，返回后也需要扫描数组寻找就绪项。当监听集合很大、每次只有少量连接活跃时，这部分工作会逐渐明显。&lt;/p&gt;
&lt;h2 id="epoll"&gt;epoll&lt;/h2&gt;
&lt;p&gt;Linux的&lt;code&gt;epoll&lt;/code&gt;把关注集合和等待动作拆开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;epoll_ctl&lt;/code&gt;负责增加、修改或删除关注的文件描述符；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll_wait&lt;/code&gt;等待并返回就绪事件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用不必在每次等待时重新提交整个集合，返回结果也直接面向就绪项。这使它适合长期监听大量连接、但同一时刻只有少量连接活跃的场景。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt;支持两种通知方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平触发：只要描述符仍处于就绪状态，就可能再次通知；&lt;/li&gt;
&lt;li&gt;边沿触发：主要在状态变化时通知，通常需要配合非阻塞I/O，并持续读写直到返回&lt;code&gt;EAGAIN&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;边沿触发并不天然代表更好的整体性能，它换来的是更严格的处理要求。遗漏一次读取或错误处理就绪状态，都可能造成连接长时间得不到处理。&lt;/p&gt;
&lt;h2 id="epoll并不总是更快"&gt;epoll并不总是更快&lt;/h2&gt;
&lt;p&gt;如果监听的描述符很少，或者大部分描述符每次都处于活跃状态，&lt;code&gt;select&lt;/code&gt;、&lt;code&gt;poll&lt;/code&gt;与&lt;code&gt;epoll&lt;/code&gt;之间的差异可能并不重要；关注集合频繁变化时，&lt;code&gt;epoll_ctl&lt;/code&gt;本身也有系统调用成本。&lt;/p&gt;
&lt;p&gt;选择模型时更值得关注的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时需要监听多少连接；&lt;/li&gt;
&lt;li&gt;活跃连接占比有多高；&lt;/li&gt;
&lt;li&gt;关注集合是否长期稳定；&lt;/li&gt;
&lt;li&gt;是否需要跨平台；&lt;/li&gt;
&lt;li&gt;业务处理是否会阻塞事件循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I/O复用只是并发网络程序的一层。事件拿到之后如何分配计算任务、控制慢请求和避免单个连接占满循环，仍然需要在应用层解决。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/select.2.html"&gt;select(2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/poll.2.html"&gt;poll(2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/epoll.7.html"&gt;epoll(7)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
用 Redis Lua 把限流操作收拢到服务端</title><link>https://bug1024.com/posts/lua-redis/</link><guid isPermaLink="true">https://bug1024.com/posts/lua-redis/</guid><pubDate>Tue, 14 Mar 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年根据当前Redis文档重新整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;限流看起来只是计数，更需要注意的是多条命令之间的边界。假设应用先判断键是否存在，再执行自增和设置过期时间，并发请求就可能在这些步骤之间交错；如果客户端在自增后、设置过期时间前异常退出，还可能留下一个不会自动清理的键。&lt;/p&gt;
&lt;p&gt;Redis Lua的价值，是把这段逻辑放到服务端一次执行。脚本执行期间具有原子性，其他命令不会插入其中。&lt;/p&gt;
&lt;h2 id="一个固定窗口计数器"&gt;一个固定窗口计数器&lt;/h2&gt;
&lt;p&gt;下面的脚本接收一个限流键、窗口秒数和最大请求数：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;div class="table-container"&gt;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;INCR&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;redis.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;EXPIRE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tonumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;调用约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KEYS[1]&lt;/code&gt;：限流维度，例如用户或接口对应的键；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARGV[1]&lt;/code&gt;：窗口时长，单位为秒；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARGV[2]&lt;/code&gt;：窗口内允许的最大次数；&lt;/li&gt;
&lt;li&gt;返回&lt;code&gt;1&lt;/code&gt;表示允许，返回&lt;code&gt;0&lt;/code&gt;表示拒绝。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一次自增后立即设置过期时间，整个过程在同一次脚本执行中完成。它减少了客户端与Redis之间的往返，也避免了多条命令被并发请求穿插执行。&lt;/p&gt;
&lt;h2 id="原子不等于没有代价"&gt;原子不等于没有代价&lt;/h2&gt;
&lt;p&gt;Redis执行脚本时会阻塞其他服务端活动，因此脚本应保持短小、确定，并避免大范围扫描或耗时计算。把复杂业务全部塞进Lua，只是把应用层的复杂度转移到了一个更敏感的位置。&lt;/p&gt;
&lt;p&gt;固定窗口本身也有边界效应：两个相邻窗口交界处可能在很短时间内通过接近两倍的请求。如果业务不能接受，需要改用滑动窗口、令牌桶等策略，而不是继续扩充这段脚本。&lt;/p&gt;
&lt;h2 id="脚本的加载方式"&gt;脚本的加载方式&lt;/h2&gt;
&lt;p&gt;频繁调用时，可以先通过&lt;code&gt;SCRIPT LOAD&lt;/code&gt;加载脚本，再使用&lt;code&gt;EVALSHA&lt;/code&gt;按摘要执行。但脚本缓存并不持久，重启、故障转移或&lt;code&gt;SCRIPT FLUSH&lt;/code&gt;之后都可能丢失，客户端仍需处理重新加载。&lt;/p&gt;
&lt;p&gt;Redis 7开始提供Functions，适合需要随数据持久化和复制的服务端逻辑。无论选择Eval脚本还是Functions，都更适合用于确实需要数据局部性和原子组合的场景。&lt;/p&gt;
&lt;p&gt;这个例子的重点不是“如何写Lua”，而是如何把一个存在并发窗口的客户端流程，收拢成边界清楚的服务端操作。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/"&gt;Redis Lua脚本文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redis.io/docs/latest/develop/programmability/functions-intro/"&gt;Redis Functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5/">技术实践</category></item><item><title>
一次效果有限的重构，和我后来理解的网站架构</title><link>https://bug1024.com/posts/web-architecture/</link><guid isPermaLink="true">https://bug1024.com/posts/web-architecture/</guid><pubDate>Sun, 08 Jan 2017 00:00:00 +0800</pubDate><description>
&lt;blockquote&gt;
&lt;p&gt;本文最初写于2017年，2026年重新整理。保留当时的问题，但不再把架构写成组件名词的集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2016年，我参与了一次网站整体重构。现在回头看，那次重构更接近一次代码重写：清理了年久失修的代码，却没有进一步调整数据库设计，也没有补齐日志和监控。新系统看起来更整齐，但承受变化和故障的能力提升有限。&lt;/p&gt;
&lt;p&gt;这件事让我逐渐意识到，架构不是用了多少中间件，也不是图上画了多少方框。它首先回答几个具体问题：系统如何划分边界，容量从哪里来，故障发生后如何控制影响，以及变化能否以可承受的成本继续发生。&lt;/p&gt;
&lt;h2 id="先把边界划清楚"&gt;先把边界划清楚&lt;/h2&gt;
&lt;p&gt;分层、拆分和分布式部署都是手段。更难的是决定哪些东西适合放在一起，哪些变化需要彼此隔离。&lt;/p&gt;
&lt;p&gt;一个相对稳定的模块，不应因为高频变化的业务功能而反复发布；低优先级服务，也不应成为核心链路的强依赖。数据库、线程池、缓存和消息队列的隔离，本质上都在限制故障传播。&lt;/p&gt;
&lt;p&gt;边界不清时，系统表面上是一个整体，实际却很难判断一次修改会影响什么。边界清楚之后，扩容、降级和排障才有明确对象。&lt;/p&gt;
&lt;h2 id="容量不是上线前猜一个数字"&gt;容量不是上线前猜一个数字&lt;/h2&gt;
&lt;p&gt;容量规划需要从真实指标出发：请求量、峰值、响应时间、错误率、线程与连接使用情况，以及数据库、缓存和消息堆积的变化。&lt;/p&gt;
&lt;p&gt;缓存、异步和集群能够提高处理能力，但也会引入一致性、延迟和运维成本。是否使用它们，不应取决于架构图是否“完整”，而应取决于当前瓶颈在哪里。&lt;/p&gt;
&lt;p&gt;在扩容之外，还需要为系统留下余量。系统如果长期贴着上限运行，一次流量波动、慢查询或下游延迟就可能演变成事故。&lt;/p&gt;
&lt;h2 id="为依赖失败做准备"&gt;为依赖失败做准备&lt;/h2&gt;
&lt;p&gt;远程调用可能超时，数据库可能不可用，消息可能积压，配置也可能出错。可靠性设计不是假设这些问题不会发生，而是提前决定发生之后怎么办。&lt;/p&gt;
&lt;p&gt;常见手段包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为调用设置明确的连接、读取和整体超时；&lt;/li&gt;
&lt;li&gt;只对适合重试的操作重试，并限制次数和退避节奏；&lt;/li&gt;
&lt;li&gt;为非核心功能准备开关和降级路径；&lt;/li&gt;
&lt;li&gt;隔离不同优先级的资源，避免一处拥塞拖垮全部请求；&lt;/li&gt;
&lt;li&gt;对关键数据准备可验证的备份和恢复流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些措施没有统一参数。脱离业务容量和失败成本谈超时、重试次数或缓存时间，往往只是把风险换了一个位置。&lt;/p&gt;
&lt;h2 id="看不见就谈不上治理"&gt;看不见，就谈不上治理&lt;/h2&gt;
&lt;p&gt;那次重构最明显的缺口之一，是日志和监控没有跟上。代码换了一遍，但系统出了问题之后，仍然缺少足够的信息判断发生了什么。&lt;/p&gt;
&lt;p&gt;可观测性至少需要覆盖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户侧的成功率和延迟；&lt;/li&gt;
&lt;li&gt;核心业务量及其异常变化；&lt;/li&gt;
&lt;li&gt;应用、数据库、缓存和消息系统的资源状态；&lt;/li&gt;
&lt;li&gt;一次请求跨越不同服务时的关联信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;报警也不只是把阈值设得足够多。一个长期无人处理的报警，最终只会成为背景噪音。&lt;/p&gt;
&lt;h2 id="架构需要允许继续修改"&gt;架构需要允许继续修改&lt;/h2&gt;
&lt;p&gt;系统不会在一次重构后完成。业务会变，团队会变，最初正确的选择也可能失效。&lt;/p&gt;
&lt;p&gt;所以我现在更在意的不是“设计出最终架构”，而是让系统能够被理解、被观测，也能够局部替换。好的结构不保证长期不变，但可以降低下一次调整的成本。&lt;/p&gt;
&lt;p&gt;回头看2016年的那次重构，它的价值不在于结果有多成功，而在于让我第一次明确区分了两件事：整理代码可以改善局部质量，改变系统能力则需要重新审视边界、容量、故障和演进方式。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E5%B7%A5%E4%BD%9C%E4%B8%8E%E7%B3%BB%E7%BB%9F/">工作与系统</category></item><item><title>
2016年总结</title><link>https://bug1024.com/posts/2016summary/</link><guid isPermaLink="true">https://bug1024.com/posts/2016summary/</guid><pubDate>Sun, 01 Jan 2017 00:00:00 +0800</pubDate><description>
&lt;p&gt;2016年过得很快，许多事情回想起来仿佛还在不久之前。既然是年度总结，就从工作、学习和生活三个方面分别记录。&lt;/p&gt;
&lt;h2 id="工作"&gt;工作&lt;/h2&gt;
&lt;p&gt;这一年工作中投入最多的事情，是和项目成员一起完成网站重构。&lt;/p&gt;
&lt;p&gt;现在回头看，这次重构的效果比较有限。前期设计不够充分，实施过程中又对历史问题做了较多妥协。代码层面得到了一些整理，但数据库设计变化不大，日志和监控也没有同步补齐。&lt;/p&gt;
&lt;p&gt;它更接近一次局部重写，而不是系统能力的整体提升。这段经历让我意识到，重构不能只关注代码是否整洁，还要明确要解决的问题，以及如何验证结果。&lt;/p&gt;
&lt;p&gt;这一年我开始承担项目组的协调工作，但在人员有限、业务环境持续变化的情况下，团队提升没有达到自己的预期。这也让我第一次认真思考，技术之外的协作、取舍和环境因素如何共同影响结果。&lt;/p&gt;
&lt;p&gt;11月底，我离开第一家公司，从福州去了北京。换一座城市，是希望进入新的工作环境，与不同的人合作，也看看自己能够适应什么样的挑战。&lt;/p&gt;
&lt;h2 id="学习"&gt;学习&lt;/h2&gt;
&lt;p&gt;过去一年读了大量技术文章，也接触了不少大型系统的架构方案。看得越多，越容易被“高可用”“高并发”等词吸引，但许多认识仍然停留在概念层面，缺少实际验证。&lt;/p&gt;
&lt;p&gt;这种浏览拓宽了视野，也带来一些浮躁。基础知识和真实问题仍然需要慢慢积累，不能只靠阅读别人的结论。&lt;/p&gt;
&lt;p&gt;年初计划学习英语和C语言，完成情况并不理想。与其把它们原封不动地推到下一年，更重要的是缩小目标，把计划变成可以持续执行的动作。&lt;/p&gt;
&lt;h2 id="生活"&gt;生活&lt;/h2&gt;
&lt;p&gt;这一年体重回到比较合适的范围，运动也基本保持下来。我仍然每周打篮球，并见证了骑士逆转勇士获得队史首个NBA总冠军。&lt;/p&gt;
&lt;p&gt;电影依旧是生活的一部分。我看了不少港片和武侠片，也去电影院看了15部电影。现在还能清晰想起的并不多，《X战警：天启》算是其中一部。&lt;/p&gt;
&lt;p&gt;感情方面没有新的结果，但我仍然愿意保留期待，不急着为这件事设定期限。&lt;/p&gt;
&lt;h2 id="2017年"&gt;2017年&lt;/h2&gt;
&lt;p&gt;新的一年，我想继续补足操作系统、MySQL、Nginx和Redis等基础，也重新建立阅读习惯。工作之外，继续锻炼和打球。&lt;/p&gt;
&lt;p&gt;希望家人和朋友身体健康。对自己，则希望少一些仓促，多一些真正完成的事情。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item><item><title>
过去两年</title><link>https://bug1024.com/posts/past-two-years/</link><guid isPermaLink="true">https://bug1024.com/posts/past-two-years/</guid><pubDate>Mon, 12 Dec 2016 00:00:00 +0800</pubDate><description>
&lt;p&gt;毕业不知不觉已经两年半了。原本国庆节就想写这篇总结，真正动笔时已经到了十二月。此时我离开了第一家公司，也离开福州，一路向北去了北京。&lt;/p&gt;
&lt;h2 id="专业选择"&gt;专业选择&lt;/h2&gt;
&lt;p&gt;高考成绩并不理想，填报专业时对未来也没有清晰认识。后来才发现，自己选择的几个专业都在同一个院系，方向都与计算机有关。&lt;/p&gt;
&lt;p&gt;这次带有偶然性的选择，最终成了后来工作的起点。&lt;/p&gt;
&lt;h2 id="编程选择"&gt;编程选择&lt;/h2&gt;
&lt;p&gt;大学过得马马虎虎，学过C，也学过Java，毕业时选择了PHP。原因并不复杂：它容易上手，而我当时恰好偏爱简单直接的东西。&lt;/p&gt;
&lt;h2 id="第一份工作"&gt;第一份工作&lt;/h2&gt;
&lt;p&gt;校招时，我在一家公司的招聘海报上看到团队成员有大厂经历，觉得值得尝试，于是投了简历。面试官指出我表现得不够自信，我也承认这一点。最后我顺利进入公司实习，并一直工作了接近三年。&lt;/p&gt;
&lt;p&gt;这份工作让我真正进入软件行业，也让我从一个缺少经验的毕业生，逐渐能够独立承担项目。&lt;/p&gt;
&lt;h2 id="离开福州"&gt;离开福州&lt;/h2&gt;
&lt;p&gt;我曾经希望第一份工作至少做满两年半，所以即使中间产生过离开的想法，还是继续做了下去。接近三年时，团队方向和我对成长的期待已经不太一致，我决定换一个环境。&lt;/p&gt;
&lt;p&gt;既然要重新开始，我想去北京试一试。这既是职业选择，也算完成自己一直以来的一个想法。&lt;/p&gt;
&lt;h2 id="新的工作"&gt;新的工作&lt;/h2&gt;
&lt;p&gt;那段时间参加了不少面试，总体还算顺利。算法准备得不充分，因此没有进入最期待的公司。最后选择了一家后续需要转向Java的团队。&lt;/p&gt;
&lt;p&gt;以前我对Java有些排斥，到了这个阶段却愿意重新认识它。与其说是追求掌握更多语言，不如说我开始接受：工具本身没有必要成为边界。&lt;/p&gt;
&lt;h2 id="表达"&gt;表达&lt;/h2&gt;
&lt;p&gt;写到这里仍然像一篇流水账。以前有位语文老师说，我的阅读理解像在发电报，总是只有很少几个字。我确实不擅长表达，也不太习惯表现自己。&lt;/p&gt;
&lt;p&gt;但记录仍然有意义。很多当时看似普通的决定，过一段时间再回头看，更容易发现它们如何改变了后来的路径。&lt;/p&gt;
&lt;h2 id="能力与努力"&gt;能力与努力&lt;/h2&gt;
&lt;p&gt;过去我不愿正视自己与优秀同行之间的差距，后来逐渐能够接受：能力有边界，成长也需要时间。努力不能保证结果，却是我能够主动选择的部分。&lt;/p&gt;
&lt;p&gt;这几年，我想继续把精力放在基础、实践和长期积累上，而不是急于证明自己应该达到什么位置。&lt;/p&gt;
&lt;h2 id="时间"&gt;时间&lt;/h2&gt;
&lt;p&gt;我的日记里出现最多的话题之一是时间。看到比自己年轻又取得成绩的人，我常常会觉得着急。那时我只有26岁，却已经产生了时间不够用的感觉。&lt;/p&gt;
&lt;p&gt;现在看，这种紧迫感既推动我向前，也容易让我忽略已经完成的事情。把握当下并不是把每天塞满，而是知道当前最值得投入的是什么。&lt;/p&gt;
&lt;h2 id="接下来"&gt;接下来&lt;/h2&gt;
&lt;p&gt;2017年，我准备继续学习Java和C，补充Linux、MySQL、Redis、RabbitMQ、Thrift与Elasticsearch等方面的知识。同时保持阅读和运动，让生活不只剩下工作。&lt;/p&gt;
&lt;p&gt;未来会怎样还不确定。先从新的城市和新的工作开始。&lt;/p&gt;</description><category domain="https://bug1024.com/posts/">posts</category><category domain="https://bug1024.com/tags/%E7%BB%8F%E5%8E%86%E4%B8%8E%E9%98%B6%E6%AE%B5/">经历与阶段</category></item></channel></rss>