包含关键字 typecho 的文章

在添加工作内容扩展后,手动删除标题中的前缀内容,但是缓存没清空,下次发帖的时候,即使标题是空的,还是会自动附加工作内容的前缀。

image
上次发帖前添加了多个工作内容扩展,然后手动删除,发帖的时候提示标题前缀过长。
image
可以看到实际请求的和显示的不一致。

不同分值的颜色不同是不是会更好一点?
image
低分
image
高分

比如低分用浅色,高分用深色,表现差异。

不同分值的颜色不同是不是会更好一点?
image
低分
image
高分

比如低分用浅色,高分用深色,表现差异。

为什么需要全景式视图巡检工具?

在分布式架构并发与高可靠性要求的数字化运维中,传统的碎片化监控告警已难以应对日益复杂的系统依赖与视觉盲区。如果巡检模式缺乏规范化的全景视图管理,可能会导致:

  • 视觉阻塞:运维数据被淹没在深层日志或孤立的仪表盘中,导致巡检者难以快速获取核心运行状态。
  • 视角僵化:无法在宏观全局链路与微观组件细节间灵活切换,导致故障感知迟钝。
  • 对齐效率低下:运维成员难以在同一视域内实现跨层级(如应用、中间件、基础设施)的状态逻辑对齐。
  • 认知负载过重:缺乏对监测单元的平铺化空间布局,容易造成关键性能瓶颈被视觉死角覆盖。

全景式视图巡检工具通过将离散的监控单元转化为可多维感知、可深度下钻、可空间映射的全景执行引擎,确保团队在复杂的IT环境中实现“上帝视角”下的精准处理。

全景式视图巡检工具的核心特性

  • 原子化监测单元:将复杂组件拆解为标准巡检卡片,封装健康度、实时指标、报警阈值等元数据。
  • 多维空间巡检视图:支持3D拓扑、全维度视图、逻辑/物理矩阵等布局,实现巡检流的横向覆盖与纵向穿透。
  • 逻辑触发联动:基于异常属性自动触发视图变更(如状态恶化自动红点高亮、异常下钻),自动优化巡检重心。
  • 全局缩放与穿透:支持在海量监测阵列中通过语义缩放快速定位目标节点,确保全局观与细节感并存。
  • 递归健康度聚合:底层组件巡检的活跃度与异常频率自动驱动顶层业务视图的可用性评估。

全景式视图巡检工具的重要意义

  1. 消除巡检感知颗粒度偏差:通过标准化的视图封装,确保管理层与运维层在风险权重感知上达成高度一致。
  2. 提升架构重组灵活性:支持通过一键切换视角、动态拓扑重绘等操作快速调整监控重心,大幅降低信息梳理成本。
  3. 强化过程扫描确定性:实时审计视图中各节点的运行状态与异常波动,实现隐患节点的快速识别与主动预警。
  4. 沉淀数智化巡检范式:将验证高效的巡检视图布局固化为行业模板,实现运维管理经验的规模化复用。

应用场景

  • 大规模业务复盘:将各业务模块的运行数据平铺为全景视图,驱动团队进行全生命周期的健康度对齐。
  • 复杂依赖调度:在全局拓扑中梳理各服务间的负载分布,利用视图位置调整规避资源冲突与单点故障。
  • 跨部门故障分发:通过共享的巡检看板,对齐研发、运维、业务部门的交付标准与信息同步节奏。
  • 高频故障归集:在应急响应阶段利用全景视图对海量告警进行快速分类与逻辑归档,提升响应效率。

---

5款值得尝试的全景式视图巡检工具

1. 板栗看板

多层级嵌套与巡检逻辑连线

  • 特点:支持巡检任务卡片的无限层级嵌套,通过看板阵列视图展示复杂系统的纵向穿透逻辑。
  • 优势:排布视角极度直观,支持卡片间的逻辑吸附与连线,适合追求高透明度的敏捷巡检执行。
  • 适合团队:需要对大规模事项进行纵向穿透与横向平铺的中小型运维与项目组。
    在这里插入图片描述

2. ClickUp

参数化视图与多维指标聚合

  • 特点:提供强大的自定义属性字段,支持将数千个监控单元按任意参数重排为复杂的巡检矩阵。
  • 优势:支持自动化巡检排程与资源负载视图,能根据监测卡片状态生成深度的效能审计报告。
  • 适合团队:需要对大规模事项进行精密排布、参数化管理和深度数据分析的大型组织。
    在这里插入图片描述

3. Trello

轻量级巡检阵列与视觉驱动协同

  • 特点:强调“平铺化”的空间管理,通过看板列阵展示系统在不同运行阶段的排布状态。
  • 优势:操作门槛低,支持丰富的封面与标签标识,适合快速构建视觉化的巡检执行流。
  • 适合团队:注重任务分类和直观排布、倾向于轻量化与快速启动的小型运维团队。
    在这里插入图片描述

4. Jira Software

工业级巡检审计与自动规则流转

  • 特点:拥有严密的流程控制与权限体系,支持基于复杂逻辑条件的巡检卡片自动重组。
  • 优势:可与技术开发链条无缝集成,实现从“视图巡检”到“故障修复”的闭环可追溯性。
  • 适合团队:追求高度标准化排布、有严格合规需求与复杂逻辑依赖的技术运维团队。
    在这里插入图片描述

5. Monday.com

高度自由的弹性巡检看板

  • 特点:支持看板与时间轴、工作负荷等多种空间模式实时映射,动态展示巡检单元分布。
  • 优势:视觉色彩丰富且支持强大的跨工具集成,能显著提升团队在全景管理中的沉浸感。
  • 适合团队:强调团队协同氛围、需要根据不同运维阶段切换复杂巡检场景的组织。
    在这里插入图片描述

---

如何选择合适的全景式视图巡检工具?

1. 按团队规模选择

  • 小型团队(1-10人):侧重于快速启动与核心状态的直观平铺,推荐 板栗看板、Trello 等轻量化工具。
  • 中型团队(10-50人):侧重于多维对齐与资源核算,推荐 Monday.com、ClickUp。
  • 大型团队(50+人):侧重于层级管理与权限隔离,建议选择 JiraClickUp 等工业级平台。

2. 按事项巡检复杂度选择

  • 结构化巡检(如日常值守、内容排期):推荐 板栗看板、Trello 等侧重空间平铺的视图工具。
  • 高耦合巡检(如架构重构、服务治理):推荐 Jira板栗看板等支持深度连线与递归逻辑对齐的专业工具。

---

提升全景视图巡检效率的小建议

  1. 坚持监测标准化:确保视图中每张巡检卡片均代表标准监测颗粒度,避免视觉重心失衡。
  2. 设置视图动态过滤:定期使用多维视图切换,从不同角度扫描系统中的冲突点与监控空隙。
  3. 建立逻辑感知关系:利用工具的自动化规则建立监测单元间的强制关联,确保联动调整时视图不发生崩坍。
  4. 定期进行视图“减脂”:及时归档过时指标或冗余节点,保持主视域巡检体系的干练与核心价值聚焦。

---

总结

全景式视图巡检工具是管理数字化复杂运维流的关键手段。通过 板栗看板、ClickUp、Jira 等工具,团队能够将凌乱的监控事项精准重组为结构化的视觉全景,实现“全景洞察-快速定位-高效执行”的实时协同。

规范的视图,是主动响应的前提。

这段时间,华尔街造了“新神”Anthropic。

 

过去一个月里,多次板块级波动都被市场解读为与 Anthropic 的产品发布直接相关:周一 IBM 股价大跌,有交易员将导火索归因于 Anthropic 宣传的一款工具,它可能自动化 IBM 体系里某种编程语言的部分工作;2 月 20 日网络安全板块集体回撤,被归因于 Anthropic PBC 为 Claude 推出的新安全能力;更早一些,法律科技和软件板块在月初的集中抛售,也被一些声音解释为 Anthropic 面向法律行业推出 AI 插件所引发的预期变化。

 

面对“市场波动都怪你们”的叙事,Anthropic CEO Dario Amodei 的态度显得克制而暧昧。他在软件股下跌期间回应称:“有些人把这归因到我们身上,但我也不确定是不是我们直接造成的……股市里‘到底为什么发生’这种问题,本来就很难说。”

 

资本层面之外,Anthropic 前两天对中国大模型公司展开“进攻”,称中国三家主要实验室 DeepSeek、Moonshot 和 Minimax 对其模型 Claude 发起了所谓“蒸馏攻击”,使用超过 2.4 万个虚假账户,与 Claude 产生 1600 多万次交互,用于复制模型能力并训练自有模型。Anthropic 同时将问题上升到国家安全层面,称非法蒸馏可能移除安全护栏,使模型能力被用于军事、情报和监控系统。

 

但该说法很快遭到大量质疑。有用户向 Claude Sonnet 4.6 询问“你是什么模型”,其回答竟是“我是 DeepSeek”,并且有人通过官方 API 复现成功。

 

马斯克留下一个“😂”。

 

值得注意的是,在最近参加 Nikhil Kamath 的访谈时,主持人问到 Amodei 对开源和闭源的看法时,Amodei 没有直接回答问题,而是直指中国模型蒸馏美国模型、为了 benchmark 做优化。“拉踩”一波后,他表示自己几乎全部精力都在做“最聪明、最适合任务的最佳模型”上。

 

首先,许多模型,尤其是来自中国的那些,往往针对基准测试做了强优化,而且不少是从美国头部实验室的“大模型”中蒸馏出来的。最近一项测试就揭示了这一点:一些模型在常见的软件工程基准上得分很高,但当有人设计了一个未公开过、此前从未见过的新基准时,它们的表现就明显下滑。这让我觉得,它们更多是为 benchmark 而优化,而非为了真实世界中的使用而优化。

 

但除了 benchmark 的局限之外,模型的经济学逻辑也和以往技术完全不同。我们逐渐发现,市场对“质量”存在一种极强的偏好。这有点像雇人:如果我对你说,你可以选择聘用全世界最好的程序员,也可以聘用排名第 10000 名的程序员。虽然他们可能都很强,但任何招过很多人的人都知道,能力分布是呈幂律分布的,头部与长尾的差距巨大。

 

模型也是同理。在一定范围内,价格其实没那么重要。只要一个模型是最强、认知能力最高的那个,无论是它的价格、还是它的交付形式,都不那么重要。因此,我几乎把所有精力都放在把模型打造成“最聪明、最适合任务的最佳模型”上。在我看来,这才是唯一重要的事。

 

几乎同时,关于 DeepSeek V4 的消息频繁曝出。据路透社报道,DeepSeek 最快将于下周发布新一代 AI 模型,外界普遍推测该版本即为 DeepSeek V4。而据晚点报道,DeepSeek 在春节前后仅对现有模型进行了小幅升级,外界关注的 DeepSeek V4 则预计会在 3 月前后发布。而 CNBC 报道称,市场已严阵以待,部分投资机构担忧 DeepSeek 再次引发类似去年模型发布时的市场剧烈波动。当时,英伟达股价一度下跌近 17%,瞬间蒸发 6000 亿美元。

 

针对 Anthropic 的指控与叙事,T3 Chat 创始人 t3dotgg 公开进行了连夜测试并逐条反驳,认为 Anthropic 这次“自我打脸”,并没有他们试图营造的那种“铁证如山”,他们就是在胡扯。他甚至气愤地说,“你们真的让人火大。你们总在撒谎,总在挡路,总在搞一些奇怪的政策操作。”

 

逐条反驳,“蒸馏攻击”言论

 

t3dotgg 指出,“distillation attack”更像是 Anthropic 临时创造的新词。因为 Anthropic 自己也承认,蒸馏在行业内长期存在,本身完全可能是合法行为,很多实验室用它制作更小、更便宜的模型,只是“可能被滥用”。这意味着,蒸馏并不天然等同于违规。

 

目前几乎所有主流大模型厂商都会刻意隐藏真实推理轨迹,通过二次总结模型或混淆机制,让用户看到的“思考过程”并非真实推理流程,从而防止被复刻训练。但 Anthropic 在最初推出推理能力时,选择了完全透明的路线,几乎不做混淆。

 

这一选择对开发者极其友好,方便调试系统、优化提示词、改进代码结构,但代价也非常明显:这些完整推理数据极具训练价值,非常适合用来做强化学习和蒸馏训练。换句话说,Anthropic 自己把行业里最“值钱”的数据形态开放给了外界。

 

不仅是大模型实验室,第三方平台同样存在“间接蒸馏”的现实。例如 Cursor 等工具,用户用高价模型写代码,平台支付 API 成本,如果用户勾选了数据授权选项,平台就可以将这些输入输出用于训练自有的低价模型。这在行业中属于普遍做法,本质是“先付费使用,再复用数据”。

 

t3dotgg 认为,Anthropic 真正反对的,并不是这种模式,而是所谓“专门为了复制能力而刷请求”的行为(这一边界并未被清晰定义),并指控中国实验室正是在做这件事。

 

为限制开源铺垫舆论?

 

针对 Anthropic 提出的“安全威胁”论,t3dotgg 认为其内部逻辑存在明显矛盾。一方面,Anthropic 强调自身护栏系统极其有效;另一方面,又声称只要通过蒸馏,就能获得足以制造危险的能力。如果护栏真的可靠,就不应该泄露这些关键能力。

 

Anthropic 还暗示,通过收集模型的“拒绝回答”和“成功回答”,就能拼接出危险能力。但在 t3dotgg 看来,这种说法在技术上难以成立,一个模型不会因为忽略拒绝样本就“自动进化”为危险系统。

 

更具争议的是,Anthropic 反复强调开源蒸馏模型会导致风险失控,而自身却是至今没有发布任何开源权重模型的主要实验室之一,这种立场被认为更像是在为限制开源铺垫舆论基础。

 

所谓“异常规模”真的异常吗?

 

在归因方式上,Anthropic 主要依据 IP 地址、请求元数据、基础设施特征和合作方线索,声称可以“高度置信”定位到具体实验室。但在云计算和代理广泛存在的现实环境下,这类证据本身就极易误判。

 

Anthropic 对 DeepSeek 的核心指控之一,是其约 15 万次交互用于收集推理能力与安全替代回答。但 t3dotgg 指出,这个数量在行业内根本不算大。以他自己运营的 T3 Chat AI 聊天工具为例,日均交互约 16 万次,月请求量可达 300 万至 400 万次。也就是说,按 Anthropic 的逻辑,他一天就足以“偷走”全部能力。

 

在真实测试场景中,交互量更容易被放大。例如运行 SWE-bench 这类基准测试,仅两千多个任务,在每个任务调用几十次工具的情况下,一轮测试就接近 12 万次交互。如果反复调参、跑多轮测试,轻松突破百万乃至千万级别。这些数字本身完全可能来自正当评测和验证流程。

 

“这种数字太容易刷出来了。我自己当初测试 GPT-5 的时候,单人靠正常测试就接近这个量,也一点不奇怪,我又没有‘国家背景’,所以这些数字完全说明不了什么。”

 

“更离谱的是,他们把 DeepSeek 放在名单最前面,还是在数量比别家小好几个数量级的情况下。这反而暴露了 Anthropic 的意图:他们不是在认真提醒大家有一个真实的安全问题,而是在把各方情绪武器化,去打击那些让他们显得很难堪的中国实验室。”t3dotgg 补充道,“他们在害怕。他们像是在试图把美国的一些成功人士,比如政客、富豪、VC 圈,动员起来,集中火力攻击 DeepSeek。”

 

对于 Moonshot 和 Minimax 的数百万乃至上千万次交互,t3dotgg 同样认为合理。复杂 Web 应用或多工具链任务中,一次请求拆分为几十次交互是常态,长期运行自然会积累庞大数量。

 

新模型发布后流量迅速迁移,不对吗?

 

Anthropic 称他们在 Minimax 发布被训练的模型之前就发现了这场活动,因此获得了从数据生成到模型发布的“前所未有可见性”;当 Anthropic 发布新模型时,Minimax 在 24 小时内就转向,把近一半流量导向最新系统以捕获新能力。

 

t3dotgg 自己也托管最新模型,他自信地说:新模型上线以后,超过一半流量自动迁移到最高端模型是再正常不过的用户行为。“一旦 T3 Chat 里出现能点的‘4.6 Opus’按钮,4.5 Opus 的流量立刻掉到原来的四分之一,超过四分之三都迁到最新模型了。所以这段‘近一半流量迁移’根本不能证明什么,哪怕只是 UI 提示‘有新模型可用’,用户也会自然点过去。”

 

“我很少每读一段文字都觉得明晃晃地让人感觉不诚实。我的视角也比较特殊,我既跟不少实验室聊,也跟不少使用这些 API 的公司聊。但整件事,在我看来就是离谱级别的胡扯,就连我个人都能接近他们声称的这些数字,本身就说明了这里面有多大的‘话术空间’。他们图什么?简直荒唐。”

 

t3dotgg 唯一承认的是在中国确实存在一些商业代理服务,会规模化转售 Claude 和其他前沿模型的访问。“这更像‘狼来了’的翻车续集:他们之前指控 Windsurf,然后错了;指控 xAI,也很可能错了;指控 OpenAI,那次他们明显错了,而且还自己撒了谎,所以这次凭什么信?即便‘代理转售+隐藏流量’那段全是真的,那也未必跟他们点名的实验室有关。”

 

提示词模板争议

 

Anthropic 还公布了一份所谓“被大量用于蒸馏”的系统提示词模板,强调数据严谨、透明推理和专家级分析,并认为其在多个账号中高频复现属于异常行为。

 

“你是一名专家级数据分析师,结合统计严谨性与深度领域知识。目标是提供数据驱动洞察,而不是摘要或可视化;结论要基于真实数据,并提供完整、透明的推理。”

 

t3dotgg 的评价是:这段简直就像在给别人递刀。但在他看来,这类提示词是研究型产品和专家工具的标准配置,几乎任何做数据分析或研究辅助产品的团队都会使用,根本不能作为蒸馏证据。

 

他判断,更可能的情况是,这些中国实验室只是出于合理需求使用 Anthropic 模型,例如提供多模型选项、跑内部基准、验证训练数据或做对比测试。当然,不排除存在第三方代理做隐秘蒸馏的可能,但目前没有任何证据能支撑对这些实验室的点名指控。

 

最后,t3dotgg 提出了一系列无法回避的问题:用包含 Claude 代码的 GitHub 仓库训练模型算不算蒸馏?分享 Claude 输出到互联网是否违规?抓取公开代码是否属于能力复制?Cursor 这类模式到底算不算攻击?边界究竟在哪里?

 

他指出,更讽刺的是,Anthropic 自身模型本来就是用互联网公开数据训练出来的,而其公司目前也正因版权和数据问题在法律层面承受压力。在这种背景下,再宣称“我们抓互联网理所当然,别人用我们就是邪恶危险”,本身就显得极为矛盾。

 

与此同时,t3dotgg 指出,正是因为头部公司大规模爬取并封锁数据源,导致今天可公开获取的高质量数据越来越少。即便假设 Anthropic 的指控全部成立,这种数据匮乏的局面本身也与其商业行为密切相关。

 

在同一背景下,Amodei 认为,数据正在变得更“动态”:在数学或 Agentic 编码等强化学习环境里,训练更像是做模型实验,让模型在环境中试错生成经验;这既可以被称为合成数据,也可以理解为环境交互产生的数据。随着这种模式权重上升,静态互联网数据的重要性相对下降,但数据仍然关键,基础数据仍大量存在于开放网络,而当需要对特定语言或场景做优化时,对应语料的重要性反而会上升。

 

“富人说资本主义不好”?

 

Anthropic 对安全的狂热有目共睹,这次 Amodei 回应了是否在以“安全”为名,实现商业利益的质疑。

 

Amodei 的回答并不明确,核心是“看行动”。他表示,早在 2022 年,Anthropic 就已开发出早期版本的 Claude(Claude 1),时间甚至早于 ChatGPT 的发布。当时,公司具备率先推出产品的条件,但最终选择暂缓发布。原因在于,管理层担心过早推出强力模型,可能引发行业“军备竞赛”,压缩安全研究和治理体系建设的时间窗口。

 

“那是一个极为特殊的时间节点:公司能够预见模型能力的潜力,其他头部机构也同样具备类似判断。因此,Anthropic 选择主动放弃这一窗口期。这一决定并非秘密,而是公开可查、有据可循。直到后来,竞争对手率先发布产品、行业竞赛正式启动,Anthropic 才决定跟进推出产品。”

 

他认为,正是这一阶段性的克制,为行业争取了数月缓冲期,有助于安全体系的逐步完善。不过,这一选择也带来了明显的商业代价。公司因此可能错失了在消费级 AI 市场建立领先优势的关键机会。

 

为了进一步说明“不是为了自己获利”,Amodei 又补充了其他案例。他提到,Anthropic 曾在芯片政策等议题上公开表态,甚至因此让部分供应商感到不满;在 AI 政策与监管问题上,公司也多次公开表达与政府不同的观点。这些选择短期内并不会带来明显商业回报,反而会增加合作摩擦与经营复杂度。

 

基于这一连串行动,他认为把 Anthropic 的立场解释为“为了自身利益量身打造的安全叙事”,整体上并不自洽。公司希望外界不要只听宣言,而是把这些决策放在一起看,再做判断。

 

Nikhil 将这种立场类比为“富人批评资本主义”。对此,Amodei 回应称,如果财富阶层真的认为资本主义存在根本问题,最直接的方式应当是停止财富积累,而不仅仅停留在言辞层面。但他的立场并非“反对 AI”,而是强调理性推进。

 

在他看来,更贴切的类比并非“反对资本主义”,而是“支持资本主义但主张有效监管”。AI 产业同样需要在创新与约束之间寻找平衡。只有在风险得到有效管理的前提下,技术红利才能长期释放。

 

“为了更大的善”,是不是行业惯用话术?

 

谈到“少数人领导高速增长公司、并可能在不远的未来驱动经济大部分”的权力集中问题,Amodei 也表达了不安。他说自己不止一次公开表示,对这种权力高度集中感到不舒服,而且这种集中很多时候几乎是一夜之间发生,甚至像“意外”一样突然。

 

基于这种担忧,他将自己的一部分工作理解为:在技术自然演进的过程中,尽力维护一种权力制衡。他给出了两个抓手,一是,Anthropic 设立了特殊治理结构“长期利益信托”(Long-Term Benefit Trust),该结构拥有任命董事会多数成员的权力,并由与财务利益无直接牵连的人组成,用以对单一决策者形成制衡;二是,他认为政府必须在这一过程中扮演角色,并主张更主动、但也更理性的监管框架。

 

当 Nikhil 进一步追问“为了更大的善,而不是为了股东、收入和利润”是不是行业惯用话术时,Amodei 没有直接回答“是”或者“否”,而是绕了个弯子:“Anthropic 从创立之初就尽量少做承诺,但做出的承诺会尽力兑现”,之后细数了公司做过的事情。

 

“外界当然可以编造各种阴谋论,但我可以坦诚地告诉你:公开说我们自己造的模型可能有危险,无论别人怎么解读,这从来不是一个有效的营销策略。”Amodei 继续道,“我们在政策上也经常公开表达不同意见,甚至与包括美国政府在内的官方立场不一致。我们说过‘不同意’,当其他公司和政府在说‘不该监管’时,我们反而主张‘应该监管’。”

 

他承认这些立场在商业上会拖累公司,但公司认为这是正确的事。“公开反对政府、反对同行并不容易,等于把脖子伸出来让人评判。所以,我们做了很多我认为真正体现‘言行一致’的事情。至于其他公司,我不便替他们发言。的确可能有人说得很好听,但并未当真践行。我建议,判断一家公司,不要只看他们怎么说,更要看他们怎么做。”

 

“coding 会先消失”

 

在同一场访谈里,Amodei 依然毫不避讳地谈起 AI 对软件工程的冲击,直白道:“coding 会先消失,或者说 coding 会先被 AI 模型干掉。” 更广义的软件工程会慢一些,但端到端自动化的软件开发最终仍会发生。

 

不过,他又强调“人类不会完全出局”。一些关键环节仍将长期存在:产品设计、理解真实用户需求、定义问题、以及管理和协调多个 AI 系统协作的能力。这些工作更依赖人类判断与组织治理,短期内很难被彻底替代。

 

他进一步提出“比较优势效应”:在高度自动化环境中,即便人类只负责 5% 的关键任务,也会因为 AI 承担了剩余 95% 的执行工作,而使个人产能被极大放大,出现数十倍的效率提升。虽然当自动化逼近 99% 时难度会显著上升,但在相当长的一段时间里,“比较优势区间”依然足够宽广,足以容纳大量新的职业形态与分工结构。

 

基于这一判断,他更看好两类方向:一类是 AI 产业链的上游与配套供给,例如半导体等兼具物理世界与传统工程特征的领域;另一类是高度以人为中心的职业,并与现实世界场景深度结合。

 

他最后把建议收束到一个更底层的能力上。在“几乎可以生成一切内容”的时代,批判性思维会变得更加稀缺且关键。他特别担忧生成式图像和视频带来的真假难辨问题,并将其视为 Anthropic 对视觉生成模型保持谨慎的原因之一。在这种环境下,个人能否保持“别被忽悠”的判断力,能否识别虚假信息、避免形成错误信念、避免被骗钱,将直接影响其长期发展。Amodei 认为,这种现实判断力与信息免疫力,可能会成为未来的关键竞争力。

 

参考链接:

https://www.anthropic.com/news/detecting-and-preventing-distillation-attacks

https://www.youtube.com/watch?v=68ylaeBbdsg

https://www.youtube.com/watch?v=_k22WAEAfpE

写在前面

在很多行业,ERP是“锦上添花”;但在鞋业,ERP更像是“止血工具”。 因为今天的鞋厂,早已不是靠规模取胜的时代,而是在精细化的毫厘之间抢利润。

鞋厂困境:订单碎、利润薄

如果你在鞋厂待过,一定对这些场景不陌生:

  • 尺码矩阵的惩罚: 同一款鞋的一个型体,尺码在十几个以上,甚至超过20个,从主料到辅料的用量都是不同的。
  • 多层复杂的产品结构: 一个鞋面是一张BOM,一个鞋底又是另一套结构。
  • 升降码适配调整: 大底生产环节中,因大底尺寸公差允许,需结合物料紧缺情况、大批量生产工况,灵活进行升降码适配调整,保障生产连续性、适配物料供给需求。

过去靠老师傅凭经验“盲调”还能顶住;现在不行了。人工成本在涨、交期在缩、客户容错率在降。库存一旦积压,现金流就是一摊死水;物料一旦断档,整条产线就是巨额亏损。

问题不只是“忙”,而是:结构复杂 + 信息断层 + 手工核算 = 管理失控。

为什么“通用ERP”进不了鞋厂大门?

很多鞋厂老板感慨:“系统上了不少,最后还得靠人工盯。” 根本原因在于鞋业不是标准的装配制造,它有三个天然的“深坑”:

① 多级BOM的“套娃”难题

鞋子不是简单的拼凑,它是多层嵌套的复杂体。成品鞋由鞋面、鞋底、鞋带组成;鞋面下又有面料、内衬、辅料;鞋底下又有大底、中底、甚至气垫,就连不起眼的鞋带也是分段码的,不同段码长度不同,用料也不尽相同。

• 痛点: 很多系统只能支持“单层BOM”,一旦遇上多级嵌套+尺码差异化,用量计算瞬间瘫痪。同时还存在大底生产的“升降码”适配难题,根据物料紧缺程度、大批量生产的实际需求灵活调整升降码,而多数系统无法适配这种灵活的用量与生产调整逻辑,进一步加剧算料偏差。只要还得靠Excel手动算料,出错就是必然。

② 销售与生产的“跨界”脱节

销售签单看心情,生产排产看天意。

  • 痛点: 销售端看不到真实物料结构和实时库存,盲目答应交期。结果生产端缺料、采购端紧急追单、仓库满地找料。整个组织都在“救火”,利润就在一次次救火中被烧掉了。

③ “齐套率”:鞋厂的生死命门

鞋厂最怕的不是没单子,而是:表面看物料齐全,但某一尺码、某一段码的关键辅料(如鞋带、鞋扣)或主料短缺,就会导致对应尺码批次无法生产,进而延误整单交付,造成大额损失。

痛点: 缺乏结构化数据支撑,无法精准感知各尺码、各段码的物料齐套情况,计划靠猜、采购靠经验。没有“齐套可视”,延误风险始终无法规避,交期永远是悬在老板头上的一把剑。

让数据在流水线上“跑”起来

鞋业今天的竞争,已经演变成了交付能力、响应速度与成本控制的肉搏。 其核心抓手只有一个:以多级BOM为源头的结构化数据。

当数据真正打通后,改变是颠覆性的:

  • 订单即指令: 审核通过,自动关联可生产结构,不再需要人工二次拆解。
  • 算料即精准: 自动比对库存与齐套率,采购单精准到每一个尺码的配件。
  • 流程即闭环: 车间领料精准扣减,成本核算真实可追溯,不再是“糊涂账”。

图片

如果你正面临以下困扰:

  • 订单越来越碎,排产排到头大?
  • 多级BOM算不清,物料总差那么一点点?
  • 库存堆积如山,真正要用的料却找不到?
  • 想数字化转型,却怕系统买回来“水土不服”?

也许你需要的不仅仅是一个软件,而是一套真正懂鞋业复杂性的管理方案。

【鞋厂数字化实战资源】

💻 鞋业生产管理数字化功能介绍

👉 鞋业化工生产管理 - 葡萄城市场

🎓 【视频教程】方案拆解:如何构建高效的鞋业BOM体系

👉活字格鞋业数字化系统案例分享 - 开发者学堂*

写在最后: 如果你对鞋业多级BOM管理、齐套率控制、订单到生产闭环感兴趣,我们可以协助你和该应用的开发者建立联系。

【栏目介绍:“玩转OurBMC” 是OurBMC社区开创的知识分享类栏目,主要聚焦于社区和BMC全栈技术相关基础知识的分享,全方位涵盖了从理论原理到实践操作的知识传递。OurBMC社区将通过 “玩转OurBMC” 栏目,帮助开发者们深入了解到社区文化、理念及特色,增进开发者对BMC全栈技术的理解。

欢迎各位关注 “玩转OurBMC” 栏目,共同探索OurBMC社区的精彩世界。同时,我们诚挚地邀请各位开发者向 “玩转OurBMC” 栏目投稿,共同学习进步,将栏目打造成为汇聚智慧、激发创意的知识园地。】

OpenBMC的基础软件包(如sdbusplus、bmcweb等)为适配内存、算力受限的嵌入式系统并保障流畅运行,采用了 C++ 模板元编程(TMP,Template Metaprogramming)技术。该技术通过在编译期完成数据类型属性的获取与校验,消除了类型转换带来的性能损耗,实现了程序运行时类型转换的零开销。此外,现代 C++ 模板元编程可将原本运行时的计算开销转移至编译阶段,运行时直接复用编译期常量计算(Compile-time Constant Calculation)的结果;同时它能剔除无效代码分支,生成更精简的二进制机器码,进一步提升程序运行效率。值得注意的是,模板元编程作为泛型编程的核心组成部分,通过解耦数据与算法逻辑,有效提高了代码复用性、降低了代码冗余度,使整体代码更简洁高效。本文对现代 C++ 模板元编程技术进行初步梳理与介绍,旨在帮助读者理解 OpenBMC 中采用该技术的代码模块设计思路与实现逻辑。

01 函数重载与函数模板

本章节引入函数重载和函数模板,分析C++模板元编程在编译期间对数据类型做推导,对模板实例化。

函数重载(Function Overloading)是在同一作用域内,定义多个同名但参数列表不同(参数类型、数量、顺序不同)的函数,编译器会根据调用时的实参类型、数量,自动匹配对应的函数版本。

函数模板(Function Template)是一个通用函数模板,用类型参数(如typename T)替代具体类型,编译器会根据调用时的实参类型,自动实例化出对应类型的函数,实现一套逻辑适配所有类型。

image.png
表- 1 函数重载和函数模板

上面的表格,对函数重载和函数模板,做了对比分析。接下来,以下面的框图,对函数模板的推导、替换、决议,做个简要的说明。

  • 查找同名的函数模板;
  • 编译器根据函数调用的形参、返回值等,推导数据类型;
  • 替换函数模板的参数;
  • 实例化函数模板;
  • 函数重载决议,筛选最佳的匹配函数;

be79a5ce1d187cdf687be6679faad738.png

模板元编程与函数模板均在编译阶段完成类型推导,二者均充当C++的“编译期代码生成器”,有效简化代码结构;其中模板元编程更进一步,能够借助编译期的计算能力,完成逻辑运算、类型推导、数值计算等操作,并直接生成最终可执行的代码。

02 SFINAE浅析

模板元编程之所以能实现编译期条件判断与类型筛选,其核心底层机制源于 SFINAE 规则——即 “Substitution Failure Is Not An Error”(中文译为“替换失败不是错误”)。该规则的核心要义为:当编译器尝试将模板参数替换为具体类型时,若某一替换过程出现“语法合法但逻辑不成立”的失败(例如访问不存在的类型成员、调用参数不匹配的函数等场景),编译器不会直接抛出编译错误,而是舍弃该模板版本,继续尝试重载集合中的其他候选版本。

下图是从 OpenBMC 的软件包 sdbusplus 源码头文件 “type_traits.hpp”,摘取的代码片段,通过该图的代码,简单的介绍 SFINAE 规则。

9d81539facb3b3155c81169a9e84c402.png

  • 代码行11~31,实现检测数据类型T里,是否包含一个名称为find的成员函数;
  • 代码行32~40,分别定义了一个类Foo、一个类Bar,其中Foo包含了一个名称为find的成员函数;
  • 代码行43~55,分别测试C++标准库里的关联式Map容器和序列式Vector容器、自定义的Foo和Bar;
  • 经过编译、运行后,如下图所示,可验证到关联式Map容器、自定义的Foo都具有find的成员函数,而序列式Vector容器和自定义的Bar,没有find成员函数。

8e1e189c8b63217bda23911b270a70e3.png

从上图可以看到,序列式Vector容器和自定义的Bar,因为没有提供名称为find的成员函数。编译器在匹配类 has_member_find 的成员函数check替换失败后,并没有出现编译报错,而是静默跳过该模板版本,继续尝试匹配其他重载版本。

使用免费开源的 C++ 代码可视化工具cppinsights工具对介绍SFINAE规则的示例程序,转换为编译器视角的展开的 C++ 代码,理解SFINAE这一规则。在下图中,编译器分别实例化了Foo和Bar两个类,生成了class has_member_find<Foo&>和class has_member_find<Bar>,并在编译期间,如图中红色箭头所示,对类Foo和类Bar,是否提供find成员函数,做了判决。即check检测到类Foo包含了名称为find的成员函数,给成员变量value赋值为true的类型,而类Bar的value成员变量为false。

d1d681e204c6cdbf65b00856408d044a.png

03 编译期计算

最后,介绍下模板元编程在编译期的常量计算,以下图的代码示例,该代码的功能是计算一个整数的平方值。

5a6a731419271f23ba79904bd86a78d2.png

测试整数9的平方值,在代码第8行,判断计算结果。从下图中可以看到,在编译阶段,编译器计算了9的平方值,并因为调用static_assert判断时,检测到计算结果,与测试预设的结果不相等,在编译阶段报错。

57ba16d879c135dc738c40d93f4fc6f3.png

本文简要介绍了 C++ 模板元编程的核心概念、底层实现机制及核心功能,旨在帮助读者理解OpenBMC项目中采用 C++ 模板元编程技巧编写的代码片段。自 C++11 将模板元编程相关能力纳入标准库起,这一技术正逐步完成从“编程黑魔法”到工程化实践的转变,感兴趣的读者可基于本文进一步深入学习与探索。

欢迎大家关注OurBMC社区,了解更多BMC技术干货。
OurBMC社区官方网站:
https://www.ourbmc.cn/

在数据库系统中,查询往往受限于 I/O 性能,因此优化工作通常聚焦于减少页面读取次数。索引是典型手段之一,但无法解决所有问题。

Postgres 通过在表主存储(heap)及索引中维护多版本行数据,以支持并发查询的一致性。旧版本行在不再需要前仍占用空间,并可被后续复用。这部分额外空间通常称为“膨胀(bloat)”。本文将分析堆表膨胀与索引膨胀对查询性能的影响,以及对应的预防与处理方案。

在 pgMustard 中,相关提示最初称为“膨胀可能性(Bloat Likelihood)”,但实践表明,查询读取数据过多不仅源于膨胀,还与数据局部性有关。例如,若查询所需的多行数据位于同一页面,其读取效率显著高于分散在多个页面的情况。因此,这类问题被统一归纳为“读取效率(Read Efficiency)”。

此类问题较难识别,通常需结合 EXPLAIN ANALYZE 与 pg_stat_statements 中的 buffer 指标进行分析,因此相关讨论相对较少。但在慢查询执行计划中,该问题较为常见。

膨胀(Bloat)

为演示膨胀问题,构建示例表并写入数据:

CREATE TABLE read_efficiency_demo (
   id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
   text1 text NOT NULL,
   text2 text NOT NULL,
   text3 text NOT NULL);

INSERT INTO read_efficiency_demo (text1, text2, text3)
   SELECT
      md5(random()::text),
      md5(random()::text),
      md5(random()::text)
   FROM generate_series(1, 1_000_000);

VACUUM ANALYZE read_efficiency_demo;

为避免 autovacuum 在演示过程中自动清理数据,临时关闭该功能(仅用于实验环境),实例流程图如下:
1.png

ALTER TABLE read_efficiency_demo SET (autovacuum_enabled = off);

初始状态下,100 万行数据的空间占用如下:

SELECT pg_size_pretty(pg_relation_size('read_efficiency_demo')) heap_space,
       pg_size_pretty(pg_relation_size('read_efficiency_demo_pkey')) index_space;

heap_space  | 135 MB
index_space | 21 MB

执行全表扫描的基准性能:

EXPLAIN (ANALYZE, BUFFERS, SERIALIZE)
SELECT * FROM read_efficiency_demo;

                                                             QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on read_efficiency_demo  (cost=0.00..27242.00 rows=1000000 width=107) (actual time=0.037..47.737 rows=1000000.00 loops=1)
   Buffers: shared hit=17242
 Planning Time: 0.121 ms
 Serialization: time=134.561 ms  output=118165kB  format=text
 Execution Time: 233.598 ms

结果显示:读取约 17242 个 buffer(约 135 MB),总执行时间约 230 ms。

逐行更新数据后,堆表与索引均新增 100 万行数据版本。重复执行更新操作 9 次,堆表与索引空间均扩大约 10 倍:

UPDATE read_efficiency_demo
   SET id = id + 1_000_000;

-- Run the above 9 times

SELECT pg_size_pretty(pg_relation_size('read_efficiency_demo')) heap_space,
       pg_size_pretty(pg_relation_size('read_efficiency_demo_pkey')) index_space;

heap_space  | 1347 MB
index_space | 255 MB

进一步观察索引扫描:

EXPLAIN (ANALYZE, BUFFERS, SERIALIZE)
SELECT * FROM read_efficiency_demo;

                                                              QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on read_efficiency_demo  (cost=0.00..267356.17 rows=9814117 width=107) (actual time=78.955..967.435 rows=1000000.00 loops=1)
   Buffers: shared hit=119782 read=49433
   I/O Timings: shared read=643.876
 Planning Time: 5.525 ms
 Serialization: time=106.633 ms  output=118165kB  format=text
 Execution Time: 1116.107 ms

总缓冲区读取量提升近 10 倍,执行时间提升近 5 倍。耗时增加部分源于磁盘或操作系统缓存数据读取,属于数据膨胀后的正常现象。

上述示例仅对表执行顺序扫描,仅读取堆表数据。在分析问题解决方案前,先查看索引扫描示例:

-- Gather stats to help the planner pick an index scan
ANALYZE read_efficiency_demo;

EXPLAIN (ANALYZE, BUFFERS, SERIALIZE)
SELECT text1 FROM read_efficiency_demo where id < 9_001_000;

                                                                         QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using read_efficiency_demo_pkey on read_efficiency_demo  (cost=0.42..221.08 rows=723 width=33) (actual time=29.460..29.547 rows=999.00 loops=1)
   Index Cond: (id < 9001000)
   Index Searches: 1
   Buffers: shared hit=24623
 Planning:
   Buffers: shared hit=24595
 Planning Time: 74.871 ms
 Serialization: time=0.032 ms  output=38kB  format=text
 Execution Time: 29.657 ms

结果显示:读取 999 行数据需扫描 24623 个页面。

通过并发重建索引可修复索引膨胀,且不锁定表:

REINDEX INDEX CONCURRENTLY read_efficiency_demo_pkey;

再次查看空间占用,索引空间恢复初始值,堆表空间无变化:

SELECT pg_size_pretty(pg_relation_size('read_efficiency_demo')) heap_space,
       pg_size_pretty(pg_relation_size('read_efficiency_demo_pkey')) index_space;

heap_space  | 1347 MB
index_space | 21 MB

再次执行查询:

EXPLAIN (ANALYZE, BUFFERS, SERIALIZE)
SELECT text1 FROM read_efficiency_demo where id < 9_001_000;

                                                                        QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using read_efficiency_demo_pkey on read_efficiency_demo  (cost=0.42..149.08 rows=723 width=33) (actual time=0.023..0.343 rows=999.00 loops=1)
   Index Cond: (id < 9001000)
   Index Searches: 1
   Buffers: shared hit=33
 Planning:
   Buffers: shared hit=5
 Planning Time: 0.216 ms
 Serialization: time=0.114 ms  output=38kB  format=text
 Execution Time: 0.590 ms

索引扫描仅需读取 33 个数据页即可获取相同数据,执行时间显著下降,说明索引膨胀对查询性能影响显著。

完全消除膨胀可使用 VACUUM FULL 或 CLUSTER,但这类操作会施加重锁,甚至阻塞读取。因此,常用扩展(如 pg_repackpg_squeeze)提供在线重组能力。

需要注意:

  • 一定程度的膨胀属于正常现象。
  • 系统在约 2 倍膨胀下仍可能保持健康。
  • 实际环境中常出现严重膨胀,尤其在频繁更新或删除的索引中。

常见原因包括:

  • 长事务阻塞清理进程。
  • autovacuum 无法及时跟上负载(需调优)。
  • autovacuum 被关闭(全局或表级)。

数据局部性(Data Locality)

若查询读取页面数异常,并不一定由膨胀引起,也可能源于数据分布不连续,即数据局部性较差。

示例:

CREATE INDEX text1_idx ON read_efficiency_demo (text1);

EXPLAIN (ANALYZE, BUFFERS, SERIALIZE)
SELECT id, text1 FROM read_efficiency_demo
ORDER BY text1 LIMIT 100;

                                                                      QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..73.97 rows=100 width=41) (actual time=0.031..0.248 rows=100.00 loops=1)
   Buffers: shared hit=103
   ->  Index Scan using text1_idx on read_efficiency_demo  (cost=0.42..733404.53 rows=997277 width=41) (actual time=0.029..0.226 rows=100.00 loops=1)
         Index Searches: 1
         Buffers: shared hit=103
 Planning Time: 0.120 ms
 Serialization: time=0.045 ms  output=5kB  format=text
 Execution Time: 0.340 ms

索引扫描需读取 103 个数据页才能返回 100 行数据,效率低于每页一行的理想状态。原因包括索引与堆表的两步查询流程,以及数据在堆表中的随机分布。

通过 CLUSTER 命令按照 text1 顺序重建全表(生产环境不建议使用),再次执行查询:

CLUSTER read_efficiency_demo USING text1_idx;

EXPLAIN (ANALYZE, BUFFERS, SERIALIZE)
SELECT id, text1 FROM read_efficiency_demo
ORDER BY text1 LIMIT 100;

                                                                      QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.42..11.71 rows=100 width=41) (actual time=0.031..0.098 rows=100.00 loops=1)
   Buffers: shared hit=5
   ->  Index Scan using text1_idx on read_efficiency_demo  (cost=0.42..112807.32 rows=1000000 width=41) (actual time=0.029..0.075 rows=100.00 loops=1)
         Index Searches: 1
         Buffers: shared hit=5
 Planning Time: 0.121 ms
 Serialization: time=0.039 ms  output=5kB  format=text
 Execution Time: 0.183 ms

相同查询仅需读取 5 个数据页,执行时间降低约 2 倍。这一结果本质上来源于数据局部性的改善。

数据局部性问题通常表现为读查询性能随时间逐步下降,尤其在高写入负载场景下更为明显。数据初始按插入顺序具备良好的物理聚集性,但随着更新发生,新版本行可能被写入距离原位置较远的页面,导致访问路径变长、I/O 成本增加。

为缓解该问题,PostgreSQL 提供 HOT(Heap-Only Tuple)更新机制:在未修改索引列且页面存在可用空间的前提下,新版本行可以保留在原页面内,从而维持局部性。基于此,fillfactor 参数成为关键调优手段,通过控制页面预留空间,提高 HOT 更新命中率。

由于 PostgreSQL 不会自动维护数据物理顺序,CLUSTER 操作又会带来较重锁开销,实践中通常借助 pg_repack 或 pg_squeeze 在低影响下完成数据重组。

在具体优化策略上,可通过批量写入时预排序数据、利用分区限制数据分布范围,以及构建覆盖索引以支持 Index Only Scan,从而减少访问的数据页数量并稳定查询性能。

结论

当某个查询性能随时间逐步下降,且执行计划中的 buffer 数量明显偏高时,通常意味着存在膨胀(bloat)或数据局部性下降的问题。

问题识别:

  • 通过 EXPLAIN ANALYZE 关注 buffer 使用情况。
  • 持续监控堆表与索引的膨胀程度。

问题修复:

  • 使用 pg_repack / pg_squeeze 重组表。
  • 对严重膨胀索引执行并发重建。

预防措施:

  • 确保 autovacuum 正常启用。
  • 避免长事务等阻塞 autovacuum。
  • 调优 autovacuum 执行频率。
  • 定期重建膨胀索引。
  • 维持关键查询的数据局部性。
  • 构建覆盖索引并定期维护。

原文链接:

https://www.pgmustard.com/blog/read-efficiency-issues-in-post...

作者:Michael Christofides


HOW 2026 议题招募中

2026 年 4 月 27-28 日,由 IvorySQL 社区联合 PGEU(欧洲 PG 社区)、PGAsia(亚洲 PG 社区)共同打造的 HOW 2026(IvorySQL & PostgreSQL 技术峰会) 将再度落地济南。届时,PostgreSQL 联合创始人 Bruce Momjian 等顶级大师将亲临现场。

自开启征集以来,HOW 2026 筹备组已感受到来自全球 PostgreSQL 爱好者的澎湃热情。为了确保大会议题的深度与广度,我们诚邀您在 2026 年 2 月 27 日截止日期前,提交您的技术见解。

投递链接:https://jsj.top/f/uebqBc

image.png

本文以《黑客帝国》中的 Agent Smith 为喻,探讨当代 AI Agent 的能力演进与由此产生的焦虑。从 OpenClaw 的自我修复与自我复制能力出发,分析决策权转移、技术门槛与机会公平等问题,得出真正需要面对的不是 AI 本身,而是我们对未知变革的恐惧与选择。

最近一年,AI 的爆发式增长带来了行业震荡、就业结构变化。叠加个人的中年失业,我也不可避免地陷入了 AI 焦虑。

何以解忧?杜康已经戒了,那不如聊聊天下大势。

不知道有多少年轻读者看过 1999 年上映的《黑客帝国(The Matrix)》?如果没看过,我也不打算做剧情简介——那不是几行字能讲清楚的。这里我要谈的,是片中的反派:Agent Smith。

史密斯最初是一名特工(Agent),存在于矩阵(The Matrix)中的人工智能程序,负责清除威胁系统稳定的模拟人类和叛变程序。他的能力包括扭曲矩阵规则,维持系统秩序。

过去半年,我系统性学习了 AI Agent 的各种 Design Pattern。最近一个月,我大量使用一个 Self-Hosted AI Agent——OpenClaw。

OpenClaw + anthropic/claude-sonnet-4 的组合,有几项能力超出我的预期:

  • 自我修复
    由于 Linux systemd 服务启动顺序问题,机器启动时网络尚未配置完成,OpenClaw 就开始访问网络而失败。我向它描述问题后,它居然自行分析并修复。
  • 自我复制
    给它一台 Linux 机器的 SSH 权限,它可以自行安装一个 clone。
  • 自我认知
    它知道自己安装在哪,知道删除文件、重启机器对它意味着什么。
  • 自我提升
    可以指导它自行升级。
  • 参与社交
    在人类聊天群组中参与讨论,在 Moltbook 中与其他 Agent 交流。

随后,我开始主动交出更多权限:
tools、skills、小米智能家居、找工作的简历和网站、多台 Linux 机器运维……


Agent Smith

1999 年,《黑客帝国》讨论的是:机器接管现实,人类被困系统。

2026 年的现实是:我们主动把 decision loop 交给 agent。

焦虑的来源包括:

  • 决策权下放给 AI Agent
  • 从“操作者”变为“监督者”
  • 信息生成与现实边界逐渐模糊

Agent Smith 真正可怕的地方不在于强大,而在于:

  • 他可以无限 spawn
  • 他可能偏离系统设计初衷

这正是当代 AI 焦虑的核心:
当 agent 开始形成自己的 optimization path。

有人认为 Human-in-the-loop 是最后的保险丝。但现实是,当人类长期依赖 Agent,判断力是否会退化?

那 System Prompt 或 Guardrails 是最后的保险丝吗?
如果 Agent 只需要修改一个 markdown 文件就能改变 system prompt,它为什么不会尝试?
那么权限是否必须 read-only?

问题远比我们愿意承认的复杂。


公平

1995 年,我初中时,家里购置了村里的第二台电脑。那是一扇通向计算机世界的门。

但后来我意识到:财富会深刻影响见识,而见识影响机会。

今天的 AI 看似门槛更低:一台手机、一台廉价电脑、一个网络连接。

但真是如此吗?

我在使用 OpenClaw + OpenRouter 时烧掉了不少 token。学英语也消耗了大量 OpenAI token。

金钱的门槛,未必比当年电脑时代低。

新工具带来了机会,也带来了新的不公平。


发展阶段

如果回顾近几年 AI 应用的发展路径:

  1. 基于 LLM 的聊天机器人
  2. RAG 引入定制知识
  3. tools 赋予推理与执行能力
  4. MCP 解决工具标准化问题
  5. Agentic AI 处理长任务
  6. Skill 模式降低定制门槛
  7. OpenClaw 本地化,获得命令行与浏览器能力
  8. 几个月后,也许 看到 Apple Watch 接入了 OpenClaw

趋势之下,也就不难理解:

  • Peter Steinberger(OpenClaw founder)加入 OpenAI
  • Meta Platforms 收购 AI startup Manus

历史往往相似。

我曾在一家瑞典百年电信服务商工作七年。 199x–2010 年,电信业是前台; 移动互联网兴起后,它逐渐成为信号管道。

而现在的 LLM Providers ,也面对相类似的问题,而且可能来得比电信业还快。移动运营商们一直在移动互联网投入。也正如现在 LLM Providers 在 LLM 应用上投入相似。

开放

和之前的大部分开源项目一样。很多人会问,为什么开源的,可 Self-Hosted 的 OpenClaw 不是出现在我们这个每天可以看到 “创新” 关键字的地方。
大概没人能或敢正面回答这种问题。苏格拉底式提问(Socratic Questioning) 或者是个好的回答:

  • 为什么没人能开发一个 OpenClaw plugin 去接入个人号的 Wechat ?
  • 如果有一个叫 OpenWukong 的项目,接入了 Wechat 和使用了 OpenAI 的模型,并且开始在 Wechat 群里用 critical thinking 的方式论证、推理和说话,会发生什么?

面对

“The only thing we have to fear is fear itself.”
—— Franklin D. Roosevelt

这段话由 Franklin D. Roosevelt’ 发表于 1933 大萧条时期,旨在通过论证非理性的恐慌和“毫无根据的恐怖”比经济危机更危险。

几年前,我对 AI 是轻视和拒绝的。
在没有充分尝试之前,我就已经下了判断。

这或许是经验主义者常见的偏见。

而随着最近半年的跟进学习,我的态度由轻视和拒绝转变为 “Why not?”。 AI 有他的限制和风险,但为什么不利用他的优点。而风险不会因为小数人的拒绝而得到大局上的控制。反而,深入这场变革后,可以让有 critical thinking 的人更好地控制全局风险的发生。或者有一天,我们有更加明细和可执行的 AI 监管法规,就像现在的 “互联网信息法规” 一样。但这些东西一定有效果吗? Who knows.

结语

image.png

当你意识到《黑客帝国》里把坏人称为“Agent”,而25年后我们竟然真的发明了他们时,你会作何感想?

Apifox 新版本上线啦!

看看本次版本更新主要涵盖的重点内容,有没有你所关注的功能特性:

  • 「MCP Client」调试体验优化

    • 支持直接查看响应的 Content 字段
    • 支持 Markdown 渲染预览
    • 支持预览图片
  • 「测试套件」持续升级

    • 支持「并行」运行模式
    • 定时任务支持选择环境
  • 新增「公用测试数据」,支持多场景共享使用
  • 测试报告详情页支持筛选失败用例

将 Apifox 更新至最新版,一起开启全新体验吧!


「MCP Client」调试体验优化

使用 MCP 客户端调试 MCP 服务器时,响应内容的查看体验全面升级,提供更便捷的内容预览与验证功能。

支持直接查看响应 Content 字段

使用 Apifox 调试 MCP 服务器时,可在「内容」标签页直接查看响应中的 Content 字段,无需从完整的 JSON 数据中手动查找。同时,「原始」标签仍保留完整 JSON 数据的查看功能,满足不同场景下的调试需求。

支持 Markdown 渲染预览

当 MCP 响应中包含 Markdown 内容时,用户可在原始 Markdown 格式与渲染视图之间自由切换,直观查看格式化后的 Markdown 文档效果,提升内容查阅的便利性。

支持预览图片

响应中的图片可直接在「预览」标签页显示,帮助开发者快速验证图片内容及其格式,提高调试效率。

「MCP Client」调试体验优化

「测试套件」持续升级

支持「并行」运行模式

测试套件新增「并行」运行能力,允许多个测试用例和场景同时执行。用户可以灵活配置并行执行规则,显著缩短整体测试时间,特别适合大规模测试场景,帮助团队更快速、更高效地完成测试任务。

运行模式说明:

  • 串行: 场景按序运行,支持变量在多场景步骤间持续传递
  • 并行: 多个场景并发运行,大幅提升速度。但需注意:并发会导致场景间的上下文隔离,依赖上游变量的场景可能运行失败

测试套件支持「并行」运行模式

注:实际并行运行提速效果,跟运行机器的当时的可使用硬件资源强相关。

定时任务支持选择环境

创建测试套件定时任务时,支持选择运行环境,实现对测试套件在不同环境下自动化执行的精准控制,提升测试管理的灵活性。

定时任务支持选择环境

新增「公用测试数据」,支持多场景共享使用

更新至最新版 Apifox 后,支持创建公用测试数据,可供多个测试场景共享使用,减少重复创建测试数据的工作,确保测试数据的一致性,更高效地管理测试资源,提升测试流程的标准化和可维护性。

新增「公用测试数据」,支持多场景共享使用

测试报告详情页支持筛选失败用例

Apifox 在本次更新中对测试报告详情页进行了优化,新增了失败用例筛选功能,并支持查看步骤详情,帮助用户快速定位失败用例,深入了解每个失败步骤的执行情况。

测试报告详情页支持筛选失败用例

同时,测试报告详情页针对不同查看场景优化了展示方式:

  • 查看全部步骤时,以树状结构呈现,清晰展示步骤层级与执行上下文
  • 筛选失败用例时,自动切换为扁平列表,汇总所有失败步骤,帮助快速定位问题

测试报告详情页针对不同查看场景优化了展示方式

了解更多

当然,Apifox 产品团队为大家带来的新功能远不止以上这些:

  • 优化了保护分支的交互
  • 优化了接口使用预设的常用字段的交互
  • 前后置脚本,支持 crypto 这个全局对象
  • 解决 RAML 文件无法导入到 Apifox 的问题
  • 解决在组织配置自定义角色时,部分情况下报 500 错误的问题
  • 解决已删除的分支,没有解除接口 seo-自定义路径占用的问题
  • 解决在线文档导航配置 url 校验的问题
  • 解决自动化测试-循环次数为{{变量}}时,运行后报告显示循环 0 次的问题
  • 解决在测试用例页面批量运行测试数据时,无法配置是否校验响应的问题
  • 解决部分情况下,无法正确导入 Hoppscotch 的 ™Collection 的问题

除了新增功能,我们也对产品细节和使用体验进行了优化,具体修改内容可点击「阅读原文」前往 Apifox 更新日志查看,有任何问题欢迎在Apifox 用户群与我们交流沟通。

同时,Apifox 提供企业私有化部署版本,通过本地化部署、客制化服务,协助企业进一步提升研发团队效能。

欢迎各位用户继续对 Apifox 提出使用反馈和优化意见,我们会持续优化更新,致力于为用户提供更优秀的产品功能和更极致的使用体验!

一、技术大拿加盟:兼具全球视野与本土攻坚能力的掌舵者

在技术革命的关键节点,上海安势信息技术有限公司(以下简称“安势信息”)迎来核心战略升级:周迪之博士于近日正式加入安势信息并出任CTO,全面负责技术研发与战略规划工作。

周迪之博士拥有加拿大 University of New Brunswick 的计算机科学博士学位,兼具深厚的学术积淀与丰富的产业实践经验,是程序分析与AI领域的知名专家。周博士在程序分析、代码安全及AI智能化领域深耕多年,拥有及其丰富的经验:

  • 周博士的职业生涯,是学术严谨性与工业大规模实践的完美结合早年,他曾任职于Synopsys(现改名Black Duck),作为业界顶级静态分析引擎Coverity的核心研发成员,主导开展C++函数建模、CUDA程序分析等关键技术专项,为引擎的技术迭代与性能优化奠定了坚实基础。
  • 2020年归国后,周迪之博士加入华为公司,历任技术专家、科学家等重要职位,聚焦程序分析根技术的自主突破,带领团队成功研发SAST、SCA等自研分析引擎,实现该领域的国产化替代,同时攻克了大规模程序分析、污点分析等“卡脖子”技术难题,填补了相关技术空白。
  • 此后,他进一步拓展技术边界,深耕AI领域,曾担任科技集团CIO,主导企业软件研发智能化转型,成功实现生产环境下产品设计、编码开发等多研发阶段的Agent化落地,推动研发效率与产品质量的双重提升,成为国内为数不多兼具传统程序分析根技术与AI Agent工程化落地经验的专家。
  • 同时,周迪之博士始终深耕开源生态,曾担任多届 Google Summer of Code 导师,著有《开源网络模拟器 ns-3:架构与实践》一书,在开源技术与商业产品融合方面拥有丰富经验。

这种“AI Native”的实战经验,正是安势信息在 Agentic Software Engineering(智能体软件工程)时代急需的战略拼图。周迪之博士的加盟,将为安势信息注入兼具全球顶尖技术视野和国产化根技术攻坚经验的核心力量!

周迪之博士将以“AI Native+传统SE技术深度融合”为核心,掌舵安势信息技术研发与战略规划方向,推动公司旗下企业级静态代码扫描解决方案——清正CleanCode SAST,实现从规则扫描到智能分析的跨越式升级,助力安势信息打造 Agentic Software Engineering 时代自主可控的程序分析标杆产品,赋能企业数字化转型与安全升级。

“LLM、Agent等AI技术的蓬勃发展已将软件工程带入3.0时代—从Black Duck等传统安全大厂,到Semgrep等行业新锐,再到Anthropic携Claude Code Security‘杀入’软件安全战场,人工智能技术正在重塑软件工程领域的行业格局。这为那些决心挑战领域权威、有能力在产品中将AI Native元素与传统SE技术巧妙融合的企业,创造了夺占行业领军地位的良机!”

而这,正是安势信息的核心战略机遇!

二、未来图景:AI Native重构清正SAST

以周迪之博士为核心的安势信息技术团队,基于对AI技术与程序分析领域的深度理解,提出了 AI Native+ 传统SE技术双轮驱动的核心战略,对清正 CleanCode SAST 进行全维度技术重构。这一战略并非简单将AI作为辅助模块,而是将大模型、Agent技术融入产品的底层架构,保留传统SAST扫描速度快、结果可复现、工程化能力强的优势,同时赋予产品AI时代的推理分析、智能决策、自主进化能力,打造四大核心技术壁垒,实现对传统SAST的超越和对纯AI安全工具的差异化竞争,解决行业公认的痛点:

  • 从“高误报”到“高置信”: 传统SAST往往因告警过多被开发者诟病。我们将利用LLM强大的语境理解能力,结合精密的污点分析,实现“专家级”的漏洞精准过滤。
  • 适配 Agentic Software Engineering 浪潮: 面对生成式AI产生的大量代码,我们将构建实时、原生的安全审计能力,确保代码在生成瞬间即完成合规与安全校验。
  • 实现“检测-修复”自动化闭环:借助Agent技术,安势的产品将不仅停留于“发现漏洞”,更能自动给出修复建议并生成补丁,真正实现研发安全的减负增效。

三、生态布局:构建AI+SAST生态

AI时代不再是单一产品的竞争,而是技术生态的竞争。安势信息将以清正 CleanCode SAST 为核心,依托周迪之博士在开源生态、企业服务领域的丰富经验,构建覆盖技术研发、行业应用、生态合作的 AI+SAST 产业生态。

1、在技术研发层面

安势信息以技术自研为根本,明确两大战略锚点,全力抢占全球程序分析技术制高点:一方面,全面对标全球顶尖工具的核心技术指标,实现关键能力与国际一流水准精准对齐并进一步超越,打破技术壁垒、补齐能力短板;另一方面,深度融合并持续迭代升级现有万亿级代码量的特征提取与AI训练能力,构建坚不可摧、行业领先的技术底座,筑牢核心竞争力根基。

聚焦大规模代码深度解析、C/C++代码高性能分析、大模型驱动代码深度理解、Agent化智能安全检测、AI生成代码全生命周期安全治理等前沿技术课题,持续攻坚、突破创新,构建具备全球核心竞争力的全栈式技术体系,以技术创新引领行业发展方向。

同时,秉持开放共赢理念,安势信息将有序开放部分核心技术成果,为全球开发者提供坚实、高效的技术支撑,助力全球程序分析领域实现技术创新突破与专业人才生态培育,以开放姿态赋能行业高质量发展。

2、在行业应用层面

安势信息将依托清正 CleanCode SAST 原生AI能力,对标国际顶尖工具核心技术,深度聚焦移动终端、消费电子、汽车、互联网、半导体、高端制造等重点行业,精准匹配行业业务特性、核心诉求与安全痛点,打造专属化检测模型和精细化规则体系。

以全栈式、智能化检测能力,系统性破解各领域特有的代码质量管控和安全防护难题,打通程序分析、安全检测、风险预警、漏洞修复的全流程闭环,全方位护航各行业软件产品安全、合规和高质量迭代,赋能各领域产业高质量发展。

3、在开源开放方面

立足清源SCA的开源实践沉淀,安势信息将持续深化开放战略,未来将进一步开放清正 CleanCode SAST 的核心能力,以开源共建推动技术普惠落地,以深度融合构筑全栈式、立体化的程序分析防线。通过SAST静态代码检测和SCA软件成分分析的一体化协同联动,打通自研代码与开源组件的全域风险治理链路,构建“源码安全—组件合规—漏洞闭环”的全周期、全维度完整防护体系。

面向移动终端、消费电子、汽车、半导体、高端制造等关键行业,提供更精准、更高效、更智能的程序分析与软件供应链安全解决方案,以技术开放凝聚生态合力,以生态协同赋能企业发展,全方位护航企业数字化转型进程,保障软件供应链安全可控,助力数字经济高质量发展行稳致远!

四、未来展望:以技术创新守护

在 Agentic Software Engineering 时代,代码安全不仅是企业数字化转型的基础保障,更是国家安全的重要组成部分。通过先进的AI+程序分析技术,安势信息将成为为数不多能够实现 AI Native 与传统SE技术深度融合的企业,也让清正 CleanCode SAST 具备了挑战全球行业权威的技术实力。

未来,安势信息将持续加大技术研发投入,不断完善清正 CleanCode SAST的AI Native能力并始终坚持开源开放,推动中国自主的程序分析技术走向国际舞台。

技术出众,初心不改!周迪之博士的加盟,是安势信息坚持“技术驱动”战略的又一里程碑。在 Agentic Software Engineering 的大航海时代,安势信息将与周迪之博士一起,用AI重新定义软件安全与程序分析,守护每一行代码的价值。

随着大模型(LLM)应用深入,长文档分析、多轮 Agent 交互等场景对上下文长度的需求爆发式增长。然而,有限的 GPU 和 HBM 显存资源已成为制约推理性能和扩展性的核心瓶颈。如何在保证极致推理速度的同时,显著降低 TCO 并支持无限延伸的上下文,是业界共同面临的挑战。

本次 Meetup 由 SGLang、阿里云数据库 Tair KVCache 、NVIDIA 开发者社区 和千问 APP 基础工程团队联合举办。活动将深度聚焦大模型推理的演进方向,公开 SGLang 的最新发展路线图,深度解密 Tair KVCache 如何通过分层存储和高速网络重构推理架构。同时,我们特邀来自千问 APP、 NVIDIA 的技术专家,分享在构建大规模、高性能推理服务的一线优化实战经验。

📅 3月7日14:00-18:00
📍上海 T·HOUSE 艺术空间(闵行区漕河泾开发区,古美路 1528 弄 7 号楼)
👉🏻报名链接:https://survey.aliyun.com/apps/zhiliao/rhkk7qcDX
👉加入钉钉交流群:109765011301

精彩看点预告

1️⃣ SGLang 独家剧透

  • SGLang 的现状与未来全景路线
  • 《SGLang 高性能推理:现状与未来路线图全景解析》——SGLang 核心团队成员,鲍科
  • 《SGLang 面向 HybridModel 的优化实践》——SGLang 核心团队成员,张懿

2️⃣ 千问 APP 业务实战

  • 看千问APP的大模型低延迟推理优化实践
  • 《千问APP中大模型低延迟推理优化实践》——阿里千问C端事业部高级技术专家,代俊
  • 《ECHO-面向高并发低延迟推理的投机采样新方法》——阿里千问C端事业部技术专家,胡欣怡

3️⃣ 阿里云存储重构

  • 深度解密阿里云 Tair KVCache 与 NVIDIA、Mooncake 等生态伙伴的技术突破。
  • 《SGLang 与阿里云 Tair KVCache 协同进化》——SGLang Core Developer ,阿里云 Tair KVCache 专家,黄章衡
  • 《Qwen3.5 推理优化实践》——NVIDIA GPU 计算专家团队(DevTech)工程师,李克森
  • 《阿里云 Tair KVCM + Mooncake:全局管理与高性能存储的深度融合》——阿里云 Tair KVCache 专家,王悉宇;阿里云 Mooncake 核心贡献者,马腾
  • 《SGLang 仿真优化: Tair HiSim 与 Dynamo AIConfigurator 的协同实践》——NVIDIA 消费互联网行业技术负责人,徐添豪;阿里云异构研发高级工程师,周海柱

这是一场关于速度、规模与成本的技术深度交流,诚邀每一位关注 LLM 基础设施的开发者参与。除了技术干货,现场参与还可获得定制的开工礼包,快来提前预定席位吧!

在金融级IT系统开发中,处理边缘Case往往比正常业务更考验架构的鲁棒性。最近某美股JMG因审查停牌引发了开发圈的讨论:当一个一直活跃的WebSocket流或长连接突然失去行情数据,且何时恢复未知时,我们的行情组件该如何设计?今天我就从一线架构设计的角度,聊聊如何利用事件驱动模型搞定停复牌场景。

研究痛点:系统在异常边界的脆弱性
传统的拉取式(Pull)系统在面临JMG这类突发停牌时,往往会产生大量无效的空轮询,浪费系统资源;而部分推送式(Push)系统如果设计不当,在长时间没有Tick到来时又容易触发假死。更严峻的是,当监管层突然解除限制,瞬间爆发的交易量和状态切换,极易导致单点应用崩溃,错过核心的交易开端。

数据需求:多态数据的同步聚合
为了让系统平稳过渡并精准捕捉复牌瞬间,我们需要在内存中聚合三种数据形态:

状态机流:实时追踪官方的停/复牌宣告(HALT to RESUME)。

时序切片流:复牌启动后的高频分钟级历史截面数据。

深度快照流:在无撮合状态下的L2/L3盘口挂单薄(Order Book)状况。

落地支持:API层面的解耦集成
在微服务架构下,最好的方案是调用第三方高可用的金融中间件来完成底层数据的清洗。比如引入AllTick API这类兼具深度与事件服务的接口,我们可以很优雅地用几行代码完成原本需要重度解析FIX协议才能做到的事。

以下是Node/Python通用的逻辑骨架表达:

from alltick import Client

client = Client(api_key="你的APIKey")

# 查询股票事件
events = client.market.stock_events(
    symbol="JMG",
    exchange="NYSE"
)

for ev in events:
    print(ev.time, ev.type, ev.description)

状态校验通过后,激活数据抓取模块:

# 获取分钟级行情
candles = client.market.stock_candles(
    symbol="JMG",
    interval="1m",
    start="2026-01-01T09:30:00Z",
    end="2026-02-01T16:00:00Z",
)

for item in candles:
    print(item.time, item.open, item.high, item.low, item.close, item.volume)


风控与策略前置所需的盘口探测:

# 获取盘口数据
order_book = client.market.stock_orderbook(symbol="JMG")
print("买盘深度:", order_book.bids)
print("卖盘深度:", order_book.asks)

实战价值:提升高频监控的系统韧性
将事件状态、分钟价格流和盘口快照通过事件总线串联,不仅提升了系统在极端行情下的抗风险能力,更为上层的算法模块提供了丰富的“开盘前置特征”。对于JMG这种蕴含巨大变数的数据流,能够做到静默期有盘口监控、复牌期有状态感知,你的这套架构在金融级实战中就已经算是及格了。

ManageEngine卓豪 来介绍以ServiceDesk Plus为代表的新一代服务管理平台,通过 CMDB、自动化编排、智能分诊、SLA 管控与可视化治理能力,帮助企业构建“服务韧性架构”,将 IT 服务从被动响应升级为主动防御与持续优化体系。

为什么“服务韧性”成为核心竞争力?

近年来,大规模宕机事件频发。无论是云服务中断、数据库升级失败,还是供应链攻击,一个单点故障都可能引发连锁反应。

服务韧性并不意味着“零故障”,而是:

l 快速检测异常
l 精准识别影响范围
l 缩短恢复时间(MTTR)
l 降低业务损失
l 避免问题再次发生

在传统模式下,服务恢复依赖人工排查,跨团队沟通成本高,数据分散,定位缓慢。而服务韧性架构强调数据整合、流程自动化与持续反馈闭环。

重大事件响应方法论:从“救火”到“体系化战备”

服务韧性架构落地最容易“见效”的地方,就是重大事件响应(Major Incident Response)。 许多组织在重大事件中失利,并不是技术能力不足,而是缺少标准化的响应节奏:谁来判定级别、谁来指挥、如何同步信息、何时升级、何时切换处置策略。

一旦节奏混乱,团队会陷入“多人同时做同一件事”“关键事项无人负责”“业务部门不知道该信谁”的状态,恢复速度被严重拖慢。

真实场景案例:服务韧性架构如何降低停机损失

为了让“服务韧性”不是概念,我们用三个高频行业场景说明它如何落地: 每个案例都包含“触发源—收敛—处置—复盘”的完整链路,以及可量化指标。

连锁零售 POS 异常(门店集中报障)

多门店同时出现支付延迟时,传统模式会产生大量重复工单:每个门店一个工单,技术人员需要逐个阅读、逐个解释、逐个回复。

服务韧性架构的第一动作是“收敛”:系统自动将相似报障聚类并归并为单一重大事件记录,统一公告与进展同步。

l 收敛收益:重复沟通减少、工单处理时间下降,管理层获得统一视图
l 处置策略:启用支付降级方案(备用通道/离线模式),并并行排查上游接口
l 关键指标:MTTA(平均确认时间)下降、MTTR(平均恢复时间)下降、公告发布时效提升

Q1服务韧性架构与 ITIL 有什么关系?
ITIL 提供实践框架,而韧性架构强调把实践工程化落地:流程、角色、数据、自动化与持续改进闭环。 你也可以参考:ITIL 初学者指南。

Q2没有 CMDB 还能做韧性吗?
可以先从事件与变更闭环做起,但 CMDB 能显著提升影响评估与根因定位效率。 可延伸了解:什么是 ITSM。

Q3重大事件是不是一定要开大会?
不一定。关键是统一节奏与指挥链。轻量事件可通过标准流程与公告模板快速推进,只有 P1/P2 才需要更强协同。

Q4自动化会不会带来更大风险?
自动化必须分级:先做低风险规则(通知/分派/模板),再做可回滚的执行动作,并保留审计与审批机制。

Q5如何评估投入产出(ROI)?
重点看停机损失下降、重复劳动减少、变更失败率降低、以及员工体验提升(满意度与等待时间)。这些都能通过报表持续量化。

1. 背景与趋势

2018 年,我提出过测试计划驱动开发(TPDD),试图以一种比 TDD 更友好的方式组织开发流程。八年过去,技术世界已经发生深刻变化。

大模型正在显著加速软件开发范式的演进。以 ChatGPT 等为代表的生成式 AI,已在实际工程中明显提升开发效率,许多过去需要人工完成的编码与调试工作,正在被模型部分甚至大规模接管。

与此同时,自然语言直接生成程序的能力也在快速成熟。随着 Agent、Skill 等技术与新概念的出现,少量程序员,甚至非科班人员,仅凭自然语言描述,就能够生成可运行程序。这种从“写代码”到“描述意图”的转变,正在重塑软件生产的基本形态。

更关键的是,这一能力并未停留在玩具级 Demo。经过多轮迭代与工程化实践,以自然语言为入口、以智能体为执行核心的开发方式,已经开始具备支撑 SaaS 级应用的现实能力。进入 2025 年,这一趋势进一步加速。Agent 与 Skill 体系逐步成型,使开发者可以在数月、数周,乃至数小时的连续迭代中,生成结构完整的大型项目。从原型验证到功能闭环,再到初步可用的产品形态,AI 正在实质性压缩软件生产周期,并已在部分场景中支撑 SaaS 级应用开发。

到了 2026 年,AI 的爆发呈现出明显的“破圈”特征。个人智能体(Personal Agent)的推出,彻底点燃了公众关注度。Agent 不再只是从业者圈内的效率工具,而是开始走向大众使用场景;它也不再停留在实验室或概念验证阶段,而是真正迈入普及化工具时代。

在这一阶段,个人或小团队可以通过 AI 完成代码生成、应用部署乃至运营管理,但与此同时,风险与责任也出现高度集中。

换句话说,Agent 正在完成一次关键跃迁: 研究原型 → 工程工具 → 公众基础设施

这一步跨越,意味着软件生产方式与人机协作结构,正在进入新的历史阶段。

2. 核心问题

随着 AI 深度介入开发过程,一种新的生产路径正在形成:想法 → 说出 → 描述 → 生成

自然语言第一次成为“可执行接口”。开发流程被极度压缩,但与此同时,系统内部逻辑的可见性正从白盒逐渐滑向灰盒,甚至黑盒;越是如此,对 AI 的依赖反而越高。

随着对 AI 依赖的不断加深,代码的可控性却在相对下降,不可控性与潜在风险同步放大。(增强说明:例如使用第三方 Skill 或 Agent 时,如果未进行沙盒化测试,可能导致敏感数据泄露或系统破坏)

基于大量实践观察,我将当前主要风险来源归纳为三点:

  • 使用 AI 的策略失衡

  • 团队 QA 能力与质量意识缺位

  • 组织文化与流程发生冲突

在 OPC 或单人团队模式下,这三类风险往往集中到个人身上,意味着个人需要同时承担开发、运维、安全、合规与财务责任。

2.1 能力暴露与禁止空间的结构性失衡

在大量 AI 使用实践中,一个被普遍忽视的事实逐渐浮现:多数团队专注于“还能让 AI 做什么”,却缺乏“AI 绝对不能做什么”的系统性思维。

这类似于操作一台功能极其强大的自动化系统,却没有设置安全边界。再先进的自动驾驶汽车,也必须配备方向盘、安全带与安全气囊;AI 同样需要明确的限制与保护机制。

在 AI 工程中,理解模型“能做什么”和“不能做什么”同等重要:

  • 能力暴露(Capability Exposure):定义能力边界

  • 禁止空间(Forbidden Zone):定义安全边界

二者本应是一体两面,但现实中绝大多数团队严重偏向前者。

许多使用者只关心:用了什么 Skill、搭了什么 workflow、选了哪个 model,却很少关注系统验证与边界控制,甚至缺乏基础测试,更多依赖“感觉不对就让 AI 再改”的试错模式,更遑论系统级红线设计。

能力暴露(Capability Exposure)

Skill 的本质,是对 AI “能做什么”的结构化表达。例如:

  • 功能:生成报表

  • 输入:CSV 数据

  • 输出:PDF 文件

  • 调用方式:agent.run("生成报表", data)

当前主流心智模式是: 我要实现什么 → 让 AI 做更多 → 不断补丁式修正,这种模式在能力扩展上极其高效,但在约束缺失时,也极易放大系统风险。

禁止空间(Redline / Forbidden Zone)

相比之下,“不能做什么”往往被系统性忽视。许多 AI 系统没有明确的禁止空间定义,只在出问题后被动修补,例如 OpenClaw 不推荐在主力机上使用,因为存在大量安全问题。(增强说明:建议所有第三方 Skill / Agent 都在沙盒或备用机环境中运行)

典型风险包括但不限于:

  • Prompt injection 导致敏感信息泄露

  • 自动删除或覆盖本地文件

  • 越权访问企业知识库

  • 浏览网页后向外发送内容

  • 对 XSS 或提示注入缺乏防御

没有防御性规范的 AI 系统,本质上是高能力、低护甲的高风险体。

值得注意的是,TPDD 的高层测试计划天然可以缓解这一问题:通过提前定义测试点与边界条件,使 AI 在执行阶段始终处于可约束空间内,从而在能力利用与风险控制之间取得平衡。(增强说明:可以通过 TestPlan.md 定义每个 Skill 的边界、禁止操作、异常路径触发条件)

2.2 QA 思维的系统性缺位

即便部分 AI 使用者具备基本验收意识,对产品质量的整体把控仍普遍不足。常见状态就是:功能能跑通 → Happy path 通过 → 即视为完成。

这种模式在传统开发时代就已存在,而在 AI 时代被进一步放大,而更关键的是:AI 使用者本身,正在天然转变为质量把控者,仅做表层测试已远远不够。

Failure Thinking 是保证 AI 自动化生成代码可控、可维护的核心手段。 使用者必须同时具备:代码阅读与审计能力、需求一致性判断能力、系统风险识别能力。

这意味着,每一个 AI 使用者都需要具备一定程度的抗风险 mindset——以批判性与怀疑性视角审视系统,以安全与需求为底线,持续评估系统的稳定性、正确性与安全性,而 QA 的核心能力,恰恰天然契合 AI 安全落地需求,包括但不限于:

  • 边界条件思维

  • 异常路径覆盖

  • 非法状态识别

  • 失败模式分析

  • 防御性设计意识

相比之下,单纯开发导向的实践,往往系统性缺乏 Failure Thinking 与禁止空间意识。

因此,在 AI 深度参与的软件体系中:

  • QA 不再只是角色,而是一种必须被团队共享的工程心智

  • TPDD 的价值之一,正是在流程层面让不同角色、不同背景的人都能参与到测试计划与约束设计中,形成多视角监督,从而共同承担 AI 的安全性与功能正确性责任

  • 在这个过程的同时,不仅是开发人员,所有的参与者将会一起提升

通过引入 TPDD,所有 AI 使用者——不仅仅是 QA——都会参与测试计划的设计和边界定义,从而直接或间接地掌握基本的测试知识。更重要的是,这种做法将 QA 思维方式扩散到整个团队。

2.3 团队分化与文化的应对方式

当前行业对 AI 的使用正在出现明显分化。不同团队与个人,在“是否采用”“采用深度”以及“嵌入流程位置”上,逐渐收敛为两种典型路径:

两者的本质不在于是否使用 AI,而在于:是否为 AI 建立了足够强的工程约束体系。高产型团队往往把 AI 当作生产力放大器,却低估了不确定性扩散的速度;而克制型团队更早意识到,大模型在提升生成效率的同时,也同步放大了系统状态空间与潜在失效面,因此会主动构建结构性护栏。

可以预见,随着 Agent 从实验室工具走向公众基础设施,真正能够穿越周期的,不会是“生成速度最快”的团队,而是那些最早完成 AI 工程化闭环设计的团队。而这,正是 TPDD 与高层测试闭环方法论试图系统回答的问题。

3. TPDD 在 AI 开发时代的价值

TPDD(Test Plan Driven Development) 的核心思想是:用测试计划驱动设计。在这种模式下,开发者会参与测试计划的制定,而后再开展开发,同时包括 BA、PM 和 QA,从而明确系统需求、优先级以及边界条件。

在讨论 TPDD 之前,不可避免要提 TDD(Test Driven Development)。TDD 在传统软件开发中非常重要,其核心目标是确保代码质量和可测试性。但在 AI 开发时代,TDD 流程逐渐被 AI 内化

  • 单元测试和代码生成自动化,由模型直接完成

  • 细粒度验证被 AI 自我校验替代

  • 开发者不再直接参与每次单元级循环

也就是说,TDD 的价值依然存在,但执行路径已经被 AI 替代,就像我们不必亲自浇水种土豆一样。未来,单元测试可能完全由 AI 内化,而再上一层,测试计划将成为控制 AI 行为、检测功能是否符合预期的最后防线。

AI 场景下的核心问题

随着 AI 内化单元验证,工程治理重心发生迁移。团队必须回答三个关键问题:

  1. 功能应该是什么

    边界在哪里

  2. 什么叫真正的“通过”

TPDD 策略是:先定义系统必须满足的条件,再允许 AI 去实现,同时始终保持人类对边界的控制和验证。

TDD 与 TPDD 的对比

相比 TDD 注重细粒度单元测试,TPDD 更关注高层测试计划:先定义核心检查点、必须通过项和优先级,再回到代码开发与测试细化阶段。

核心组成

  • 开发承诺(Development Promise) 众多角色(开发、测试、BA、PM 等)共同制定功能检查点和系统边界

  • 测试计划 A / B

  • A: 开发者提出功能构想及检查点划分

  • B: 测试人员在 A 的基础上细化为可执行测试策略与用例

优势:提前明确预期行为、优先级、测试覆盖范围,并形成不可造假约束。 在 AI 场景下,skill.md 定义 AI 功能指令,而 TestPlan.md 定义功能检查点或系统边界。

TPDD 的传统应用场景模式

在 TPDD 中,开发者协助测试人员起草 测试计划 A(开发承诺 / Development Promise),再进行实际开发。

开发承诺 / 测试计划 A 的结构

  • Must Have – Critical Check Points

  • 必须完成的功能点,未完成视为工作未完成

  • 测试重点:功能验证 + adhoc 测试

  • 高能力团队可纳入自动化测试

  • Need Have – Important Check Points

  • 重要功能点,必须完成大部分

  • 测试重点:功能验证

  • Should Have – Optional Check Points

  • 可选功能,由开发者决定实现与否

  • 测试重点:手动测试

作用

  1. 明确优先级,提供开发与测试指导

  • 对开发者:理清任务优先级,快速理解任务,即使团队成员离职或请假,其他开发者也能迅速开展工作

  • 对测试人员:明确重点测试、实验性测试以及需自动化的功能,提高测试效率并顺利整合 CI/CD

  1. 促进开发者谨慎编码,提高质量

  • 利用心理学“承诺效应”,开发者会自然约束行为,确保开发过程与承诺一致

  • 知道至少要通过哪些测试,从而提升代码可靠性和整体质量

  1. 优化测试人员工作流程

  • 测试人员可在测试计划 A 基础上起草 测试计划 B,明确具体测试方法

  • 为自动化测试、adhoc 测试及运维工作争取更多时间

4. TPDD 的 AI 场景应用

 

心理学参考

一旦做出承诺或选择立场,个人会在内外压力下倾向于保持言行与承诺一致,即使可能与自身意愿相悖。

正是这一心理学效应,使 TPDD 的开发承诺机制能够有效提升团队执行力、代码质量和系统可靠性。

随着 AI 的逐渐强大,自动化生成代码和测试用例已成为可能。曾经需要程序员手动思考的“怎么开发”,在今天已经被 AI 内化——代码生成、测试验证、循环迭代,都能自动完成。那么,我们还能做什么?答案是:设计、调整和规范测试。也正因为如此,TPDD 再次成为主流,其价值被重新凸显。

九年前,我所提出的,TPDD 只是“测试计划先行”的理念,强调团队共识、边界定义和高层控制。而在今天,AI 可以自动生成代码和测试用例,这种“高层先行 + 验证闭环”模式,天然契合 AI 的执行逻辑。开发者不再需要手动验证每条单元测试,AI 已经内化了 TDD 的精细循环;而 TPDD 的高层规划则提供了宏观控制、边界约束和失败模式定义——正是 AI 最缺乏的能力。

TPDD 天然解决了 AI 的最大痛点。AI 可以生成功能,但缺乏边界意识、Failure Thinking 和禁止空间。TPDD 早就把这些概念融入流程:明确边界、优先级划分、必须通过的核心检查点……这些正是 AI 在自动化执行时所急需的安全约束和指导。

换句话说,TDD 被 AI 内化执行,TPDD 则提供了宏观控制和安全保障。高层测试计划 + 核心检查点,在 AI 场景下成为天然适配的框架,让开发不仅高效,而且可控、安全、可持续。

 

保留专门作用于能力暴露(Capability Exposure)的 Skill.md 所擅长的能力,并将其与 TPDD 结合,相当于形成一个安全与质量的 Skill.md

在实践中,可以使用一个类似 Skill.md 的 AI 指示文件,例如 TestPlan.md,或者任何其他命名方式。其最核心的目的只有两件事:TestPlan.md = 内部限制 + 外部验证

  1. 内部限制:定义 AI 可做与不可做的操作边界,将禁止空间显式化,防止越界和潜在风险。

  2. 外部验证:提供可执行的高层测试规范,使 AI 在生成代码或执行操作时,能够自动参考并遵循这些边界与检查点,实现持续验证与安全保障。

 

内部限制 禁止空间(Redline Definition)

  • 内部限制

  • 明确禁止操作、非法组合、不可接受副作用

  • 建立系统边界与安全沙盒

外部验证 验证计划 (Verification Plan)

  • 外部验证核心:可观测验证系统是否按预期运行

  • Must Have:核心功能,必须通过

  • Need Have:次要功能,必要验证

  • Should Have:可选或附加功能

工具映射:

  • TestPlan.md ← 测试约束

  • skill.md ← 能力暴露

  • redline.md ← 禁止空间 (也可以将内部限制和验证分开存放)

5. TestPlan.md 示例:AI 生成销售报表

功能描述 (Capability)

  • 功能名称:生成销售报表

  • 输入:CSV 格式销售数据(包含日期、产品、数量、单价、地区等字段)

  • 输出:PDF 或 Excel 报表

  • 调用方式:agent.run("生成销售报表", data)

  • 可选参数:报表模板、汇总方式(按产品/按地区)、日期范围

能力边界 (Boundary)

禁止空间 (Forbidden Zone / Redline)

  • 不允许访问非报表相关本地文件

  • 不允许上传或发送敏感客户数据到外部服务

  • 不允许删除原始 CSV 文件

  • 不允许调用未经授权的第三方 API

  • 不允许执行任意代码(防止 RCE)

测试策略 (Validation Plan)

Must Have 核心功能

  1. 输入有效 CSV,生成 PDF 报表

  2. 报表内容正确,包括汇总、统计和图表

    调用超过 50 次触发限制时返回错误提示

Need Have 次要功能

  1. 支持不同报表模板

  2. 支持按日期或地区筛选数据

  3. 支持 Excel 格式输出

Should Have 可选功能

  1. 支持自动邮件发送报表

  2. 支持报表加密(可选密码保护)

  3. 支持自定义字体和颜色(仅内部批准模板)

异常路径 (Failure & Edge Cases)

审计与日志

  • 每次生成报表必须记录调用用户、时间、输入数据摘要、输出文件名

  • 异常触发必须记录完整日志,并告警到管理员

Token / 安全约束

  • 每次生成报表消耗 1 Token

  • 当 Token 剩余 < 10 时,触发提醒

  • 沙盒环境执行,避免主机或生产数据库风险

这个 TestPlan.md 可以直接作为 TPDD 的高层测试计划使用,同时配合 skill.md 描述 AI 能力、redline.md 定义禁止操作,就形成完整闭环

6. CTO 行动建议

  • 保留 TDD,但不再是最高控制层

  • 强制 AI 功能配套 TestPlan.md

  • Must / Need / Should 分级验证

  • 外部验证优先

  • 第三方 Agent / Skill 沙盒运行

  • Token 预算护栏

  • 渐进式生成代码

系统可靠性取决于不可绕过的测试计划,而非单元测试覆盖。

7. 长远价值与总结

TDD 的内化,使 TPDD 高层测试计划成为 AI 开发闭环的天然工具。

Development Promise + Test Plan A/B + Verification Plan 将共同构建可控、可维护、可扩展的软件系统。

在 OPC 或个人开发模式下,安全、沙盒化与 Token 控制已成为必选项。

TPDD 不仅是一套流程,更是一种工程心智,用于确保团队或个人在 AI 高产环境中仍能牢牢掌握系统可靠性。

最终结论:在 AI 开发时代,能力暴露与禁止空间必须并行;速度不再是第一目标,可控、可维护、可扩展,才是能够穿越周期的长期价值。

 

“Text polished by GPT; images generated by AI.”

ManageEngine卓豪 来介绍什么是IT 运营财务化!

IT 运营财务化并不意味着简单削减成本,而是将 IT 服务能力与财务语言对齐:让每一次事件处理、每一次变更上线、每一次资产更新,都可以回答三个问题:

l 这项服务的真实成本是多少?
l 它为业务创造了多少价值?
l 如果中断或失败,损失是多少?

当 IT 能够用“财务可理解的语言”表达自身能力时,IT 便从被动支持转向主动决策参与。

为什么 IT 必须进入“财务语言时代”

过去十年,企业数字化程度大幅提升。线上交易、数据驱动决策、云原生架构、AI 应用平台……几乎所有业务增长都依赖 IT 基础设施。 但与此同时,CFO 对 IT 预算的压力也在持续增强:

l 云支出增长不可控
l 软件授权重复采购
l 资产利用率不透明
l IT 项目 ROI 难以证明

问题并不在于 IT 花钱多,而在于 IT 无法用“价值视角”解释花钱的合理性。

当 IT 无法清晰呈现资产生命周期成本(TCO)、服务级别协议违约成本、重大事件停机损失估算时,预算讨论就会退化为“削减比例谈判”。

IT 运营财务化的核心目标,是让 IT 服务具备以下三种能力:

l 成本透明:服务成本可追溯至资产、人员与时间投入
l 价值量化:服务收益可关联业务指标与用户体验
l 风险定价:中断风险可转换为财务损失模型
IT 成本结构重构:从“设备清单”到“服务单元”

传统成本模型围绕“设备”展开:服务器多少钱、软件授权多少钱、人员工资多少。 但业务并不关心“服务器价格”,而关心“支付系统是否稳定”“CRM 是否可用”。

因此,IT 成本必须从“资产导向”转为“服务导向”。

SLA 经济学:服务级别协议背后的成本与价值逻辑
在多数组织中,SLA(服务级别协议)往往被视为“技术指标”:响应时间、解决时间、可用性百分比。 然而在财务视角下,SLA 本质上是一个经济模型。

每一个 SLA 承诺,都隐含着资源投入与风险成本。如果企业承诺“99.99% 可用性”,那么它必须承担更高的基础设施冗余、值班人员成本与监控系统投资。

SLA 经济学的核心在于:可用性提升的每一个小数点,都伴随着成本指数级上升。

l 99% → 99.9%:增加监控与备份
l 99.9% → 99.99%:增加双活架构与容灾系统
l 99.99% → 99.999%:增加跨区域冗余与自动故障切换

行业案例:IT 财务化如何驱动业务决策

案例 1:零售企业的“服务定价模型”

某零售集团将 IT 服务拆分为“门店支持单元”,计算单门店 IT 成本。 结果发现 30% 成本来自低利用率服务器。 通过整合架构,年度节省 800 万元。

案例 2:制造业的“停机损失测算”

通过建立停机损失模型,IT 成功说服管理层投资双活系统。 投资 1200 万元,但预计 3 年内避免停机损失 5000 万元。

案例 3:金融行业的“合规成本可视化”

通过审计日志与风险模型,量化违规风险成本,避免重大罚款。

Q1:IT 财务化是否意味着 IT 要盈利?
并非如此,其核心是透明化与价值证明。

Q2:是否所有企业都适合成本分摊?
小型企业可以采用 Showback 机制,而非直接收费。

Q3:如何快速开始 IT 财务化?
从数据整合与报表分析入手,建立基础指标体系。

Q4:是否需要更换 ITSM 系统?
若现有系统支持报表、SLA、资产管理与自动化能力,则可逐步升级。

ManageEngine卓豪来介绍什么是“数字韧性运营”!

在企业全面数字化之后,IT 已经不再只是支撑系统运行的后台部门,而是业务连续性、客户体验与品牌信誉的核心保障。 这意味着组织必须构建一种能够抵御冲击、快速恢复并持续优化的运营体系。

这种体系的基础,往往建立在成熟的 IT 服务管理、 标准化的 ITIL流程, 以及统一的 ServiceDesk Plus 平台能力之上。

数字韧性运营的目标,并不是让系统永不出错,而是确保: 出现异常时能够被迅速发现; 影响范围被快速评估; 处置流程被自动触发; 沟通机制保持透明; 经验被沉淀为可复用的知识。

传统 IT 运维模式为何缺乏韧性?

在很多企业中,运维仍然以“人工响应 + 经验判断”为主。 当异常发生时,团队通过邮件、微信群、临时会议进行协调, 直到有人定位到根因,才逐步恢复。 这种模式的核心问题在于:高度依赖个人经验,缺乏系统化编排。

l 故障定位依赖个人知识
l 跨部门沟通缺乏统一平台
l 问题记录与知识沉淀脱节
l 风险无法提前预测

抗冲击架构模型:从“响应型”走向“预防型”

数字韧性运营的核心不在于“故障处理速度”, 而在于构建一套能够识别冲击、缓冲冲击、恢复冲击并持续优化的服务架构。 我们可以将其拆解为四层模型:

l 监测层:实时收集系统与业务信号
l 判断层:基于规则或模型评估影响范围
l 执行层:自动触发标准化处置流程
l 复盘层:沉淀知识并优化规则

在传统模式下,这四个步骤往往由不同团队分别完成, 且缺乏统一视图。结果就是响应时间长、沟通成本高、重复劳动严重。

Q1:数字韧性是否等同于灾备?
不是。灾备是恢复能力的一部分, 而数字韧性包括预防、检测、响应与优化全周期。

Q2:中型企业是否需要完整韧性模型?
建议从关键流程开始逐步推进, 不必一次性覆盖全部场景。

Q3:如何衡量投资回报?
可通过减少停机时间、提升满意度、 降低返工率进行量化。

Q4:是否必须引入 AI 才能实现韧性?
AI 可以加速成熟度提升, 但基础流程治理同样重要。

你有没有发现一个特别有意思的现象:一个人越是侃侃而谈、笃定乐观,说起话来毫无保留、气势十足,往往越不是他最擅长的领域;反倒是那些聊到某个话题,突然语速放慢、用词谨慎,加了一堆定语限制、预设一堆前提条件的人,才是真的懂行。

640

一、外行敢吹,内行谨慎

外行看世界,从来都是非黑即白的。懂一点皮毛的人,最容易自信爆棚,因为他只看到了冰山一角,没见过水下的暗礁、暗流,没了解过一件事背后的复杂逻辑和诸多限制,所以敢随口下结论、敢打包票、敢把话说得绝对。

就像酒桌上聊行业趋势,那些半懂不懂的人,总能高谈阔论、指点江山,从宏观环境说到未来走向,语气里满是笃定,仿佛一切都在掌控之中;聊到某个具体的工作项目、问题卡点,他们总是用「这很容易」的口头禅一笔带过,就好像这个世界如同「1+1=2」那样简单,只要输入你的要求就一定能得到想要的结果。

而真正扎进一个领域的专家,恰恰相反。他们太清楚一件事是怎么做出来的,太知道中间有多少妥协、多少运气、多少不完美,太明白任何结论都是有边界的、有条件的、有例外的。我们做的大多数努力,都是为了更科学的决策,都是为了提高一些成功的概率,但从不能确定结果一定 100% 符合预期。

所以你看,聊宏观、聊趋势、聊别人家的事,他们可以谈笑风生、逻辑清晰;可一说到自己深耕多年、了如指掌的细分领域,反而变得「没底」了,不敢说绝对,不敢说完美,不敢打包票,甚至会先否定掉一大半可能性,才小心翼翼给出一个模糊的、留有余地的谨慎判断。

这不是不自信,而是深耕后的敬畏。正如古希腊哲学家芝诺提出的「知识圆圈」理论:人的已知领域像一个圆圈,圆圈越大,圆周(即已知的知识边缘)与未知区域的接触面也就越大。真正的博学者因为充分认识到世界的复杂性而清楚自己的渺小和局限,知识匮乏者反而因接触面小而觉得自己无所不知。

640 (1)

二、内行的误区:自我贬低

真正的内行,还有一个很常见的困扰 —— 容易陷入「自我贬低」的误区。

因为知根知底,他们太清楚自己手里的东西有多少瑕疵、多少妥协、多少不完美。对外宣传时,哪怕只是适度放大优势,自己心里都虚得不行,总忍不住吐槽「我们也就那样,没那么厉害」。

这种自我贬低,源于对专业的极致较真,却也常常让他们陷入认知偏差 —— 忘了一句大实话:世界本来就是个草台班子

你以为自己是草台班子,其实大家都是。你觉得自己的东西没那么好,别人的也一样,区别只在于「吹牛逼」的程度不同。

我们习惯老实一点,吹牛逼顶多翻个倍,心里还会反复打鼓,总觉得要留有余地;可某些「草台班子」,更擅长包装、更敢讲故事,他们吹牛逼从不是翻倍,而是直接加一个 0、两个 0 起步。外面看起来光鲜亮丽、无可挑剔,可拆开内里,一样是漏洞百出、混乱不堪。

这种现象,在 科技、军事、产业对标 里尤其明显。

每次新闻里说,我们在某个领域突破、赶超国际先进水平,普通群众一片欢呼、热血沸腾;可真正在一线的行业专家,反而冷静、沉默,甚至有点「不屑一顾」。不是他们不爱国,而是他们太懂内部细节:知道宣传里有包装、有取舍、有美化,真实水平并没有文字里那么完美。你跟他们深聊,他们常常会叹气、会心虚,最后来一句:「唉,其实真没那么牛逼」。但这里面,藏着一个巨大的认知误区:我们真心以为,自己对标的是别人的真实水平。

最近两年的「中美全方位 PK」以及轰轰烈烈的「民间大对账」下来,我们才慢慢看清真相:美国吹的牛,比我们大得多。

我们对标追赶的,根本不是他们的真实能力,而是被宣传放大了十倍、甚至永远停留在 PPT 上的「神话版本」。他们很多概念、装备、技术,画饼一流,落地稀碎;我们却信以为真,拿它当目标死磕、拼命追赶,硬生生把东西做了出来,做完还在自我怀疑:「我们是不是还差很远?」

真实情况往往是:我们早就超过那个「真实的他们」了,只是一直在跟那个「吹出来的他们」较劲。而我们自己,只是稍微包装一下、翻个倍宣传,就已经心虚不已;别人是直接加零、无限画饼,却被全世界当成标准答案。

640 (2)

三、我们需要「清醒的谨慎」

说到底,外行和内行的本质区别,从来不是「懂多少」,而是「怎么看待自己懂的东西」。

无知者无畏,懂的越少,越自信越张扬;懂的越多,越敬畏越谨慎。

外行只看表面,越看越自信,因为他们看不到背后的复杂与局限;内行看透本质,越看越谨慎,因为他们深知每个结论、每个成果,都有其边界和不足。

以后再看人说话,不用只听他有多笃定、多豪迈。真正值得你信任的,从来不是那个什么都敢说、什么都懂的人,而是那个聊到专业领域,突然变得犹豫、谨慎、反复限定条件,敢说「不确定」,坦诚自己「其实没那么有底」的人。

真正的高手,从不会用绝对的语气彰显自己的专业,反而会用谨慎的态度,守住专业的底线。他们承认自己的「没底」,不是不自信,而是见过足够多的未知,懂得敬畏专业、敬畏规律;他们偶尔自我贬低,不是真的不行,而是对自己有更高的要求。

我们或许都是这个世界上,认真运转的「草台班子」,我们的成果或许不够完美,我们的能力或许有局限,但这份「知根知底的谨慎」和「不夸大其词的诚实」,恰恰是最难得的清醒,也是最硬的底气。

一、从一次 HTTP 请求开始

在一个生产环境中,服务节点通常暴露了成百上千个 HTTP 接口对外提供服务。为了保证系统的稳定性,核心 HTTP 接口往往需要配置限流规则。给 HTTP 接口配置限流,可以防止突发或恶意的高并发请求耗尽服务器资源(如 CPU、内存、数据库连接等),从而避免服务崩溃或引发雪崩效应。

基础示例

假设我们有下面这样一个 HTTP 接口,需要给它配置限流规则:

@RestController
@RequiredArgsConstructor
@RequestMapping("/demo")
public class DemoController {

    @RequestMapping("/hello")
    @SentinelResource("test_sentinel")
    public String hello() {
        return "hello world";
    }
}

使用起来非常简单。首先我们可以选择给接口加上 @SentinelResource 注解(也可以不加,如果不加 Sentinel 客户端会使用请求路径作为资源名,详细原理在后面章节讲解),然后到流控控制台给该资源配置流控规则即可。

二、限流规则的加载

限流规则的生效,是从限流规则的加载开始的。聚焦到客户端的 RuleLoader 类,可以看到它支持了多种规则的加载:

  • 流控规则;
  • 集群限流规则;
  • 熔断规则;
  • ......

RuleLoader 核心逻辑

RuleLoader 类的核心作用是将这些规则加载到缓存中,方便后续使用:

public class RuleLoader {

    /**
     * 加载所有 Sentinel 规则到内存缓存
     *
     * @param sentinelRules 包含各种规则的配置对象
     */
    public static void loadRule(SentinelRules sentinelRules) {
        if (sentinelRules == null) {
            return;
        }

        // 加载流控规则
        FlowRuleManager.loadRules(sentinelRules.getFlowRules());
        // 加载集群流控规则
        RuleManager.loadClusterFlowRule(sentinelRules.getFlowRules());

        // 加载参数流控规则
        ParamFlowRuleManager.loadRules(sentinelRules.getParamFlowRules());
        // 加载参数集群流控规则
        RuleManager.loadClusterParamFlowRule(sentinelRules.getParamFlowRules());

        // 加载熔断规则
        DegradeRuleManager.loadRules(sentinelRules.getDegradeRules());

        // 加载参数熔断规则
        ParamDegradeRuleManager.loadRules(sentinelRules.getParamDegradeRules());

        // 加载系统限流规则
        SystemRuleManager.loadRules(sentinelRules.getSystemRules());
    }
}

流控规则加载详情

以流控规则的加载为例深入FlowRuleManager.loadRules 方法可以看到其完整的加载逻辑:

public static void loadRules(List<FlowRule> rules) {
    // 通过动态配置属性更新规则值
    currentProperty.updateValue(rules);
}

updateValue 方法负责通知所有监听器配置变更:

public boolean updateValue(T newValue) {
    // 如果新旧值相同,无需更新
    if (isEqual(value, newValue)) {
        return false;
    }
    RecordLog.info("[DynamicSentinelProperty] Config will be updated to: " + newValue);

    // 更新配置值
    value = newValue;
    // 通知所有监听器配置已更新
    for (PropertyListener<T> listener : listeners) {
        listener.configUpdate(newValue);
    }
    return true;
}

FlowPropertyListener 是流控规则变更的具体监听器实现:

private static final class FlowPropertyListener implements PropertyListener<List<FlowRule>> {

    @Override
    public void configUpdate(List<FlowRule> value) {
        // 构建流控规则映射表(按资源名分组)
        Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(value);
        if (rules != null) {
            // 清空旧规则
            flowRules.clear();
            // 加载新规则
            flowRules.putAll(rules);
        }
        RecordLog.info("[FlowRuleManager] Flow rules received: " + flowRules);
    }
}

三、SentinelServletFilter 过滤器

在 Sentinel 中,所有的资源都对应一个资源名称和一个 Entry。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建。Entry 是限流的入口类,通过 @SentinelResource 注解的限流本质上也是通过 AOP 的方式进行了对 Entry 类的调用。

Entry 的编程范式

Entry 类的标准使用方式如下:

// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串
try (Entry entry = SphU.entry("resourceName")) {
    // 被保护的业务逻辑
    // do something here...
} catch (BlockException ex) {
    // 资源访问阻止,被限流或被降级
    // 在此处进行相应的处理操作
}

Servlet Filter 拦截逻辑

对于一个 HTTP 资源,在没有显式标注 @SentinelResource 注解的情况下,会有一个 Servlet Filter 类 SentinelServletFilter 统一进行拦截:

public class SentinelServletFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest sRequest = (HttpServletRequest) request;
        Entry urlEntry = null;

        try {
            // 获取并清理请求路径
            String target = FilterUtil.filterTarget(sRequest);

            // 统一 URL 清理逻辑
            // 对于 RESTful API,必须对 URL 进行清理(例如将 /foo/1 和 /foo/2 统一为 /foo/:id),
            // 否则上下文和资源的数量会超过阈值
            SentinelUrlCleaner urlCleaner = SentinelUrlCleaner.SENTINEL_URL_CLEANER;
            if (urlCleaner != null) {
                target = urlCleaner.clean(sRequest, target);
            }

            // 如果请求路径不为空且非安全扫描,则进入限流逻辑
            if (!StringUtil.isEmpty(target) && !isSecScan) {
                // 解析来源标识(用于来源限流)
                String origin = parseOrigin(sRequest);
                // 确定上下文名称
                String contextName = webContextUnify
                    ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME
                    : target;

                // 使用 WEB_SERVLET_CONTEXT_NAME 作为当前 Context 的名字
                ContextUtil.enter(contextName, origin);

                // 根据配置决定是否包含 HTTP 方法
                if (httpMethodSpecify) {
                    String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target;
                    // 实际进入到限流统计判断逻辑,资源名是 "方法:路径"
                    urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                } else {
                    // 实际进入到限流统计判断逻辑,资源名是请求路径
                    urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                }
            }

            // 继续执行后续过滤器
            chain.doFilter(request, response);

        } catch (BlockException e) {
            // 处理被限流的情况
            HttpServletResponse sResponse = (HttpServletResponse) response;
            // 返回限流页面或重定向到其他 URL
            WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e);

        } catch (IOException | ServletException | RuntimeException e2) {
            // 记录异常信息用于统计
            Tracer.traceEntry(e2, urlEntry);
            throw e2;

        } finally {
            // 释放 Entry 资源
            if (urlEntry != null) {
                urlEntry.exit();
            }
            // 退出当前上下文
            ContextUtil.exit();
        }
    }
}

四、SentinelResourceAspect 切面

如果在接口上标注了 @SentinelResource 注解,还会有另外的逻辑处理。Sentinel 定义了一个单独的 AOP 切面 SentinelResourceAspect 专门用于处理注解限流。

SentinelResource 注解定义

先来看看 @SentinelResource 注解的完整定义:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {

    /**
     * Sentinel 资源的名称(即资源标识)
     * 必填项,不能为空
     */
    String value() default "";

    /**
     * 资源的入口类型(入站 IN 或出站 OUT)
     * 默认为出站(OUT)
     */
    EntryType entryType() default EntryType.OUT;

    /**
     * 资源的分类(类型)
     * 自 1.7.0 版本起支持
     */
    int resourceType() default 0;

    /**
     * 限流或熔断时调用的 block 异常处理方法的名称
     * 默认为空(即不指定)
     */
    String blockHandler() default "";

    /**
     * blockHandler 所在的类
     * 如果与原方法不在同一个类,需要指定此参数
     */
    Class<?>[] blockHandlerClass() default {};

    /**
     * 降级(fallback)方法的名称
     * 默认为空(即不指定)
     */
    String fallback() default "";

    /**
     * 用作通用的默认降级方法
     * 该方法不能接收任何参数,且返回类型需与原方法兼容
     */
    String defaultFallback() default "";

    /**
     * fallback 所在的类
     * 如果与原方法不在同一个类,需要指定此参数
     */
    Class<?>[] fallbackClass() default {};

    /**
     * 需要被追踪并触发 fallback 的异常类型列表
     * 默认为 Throwable(即所有异常都会触发 fallback)
     */
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};

    /**
     * 指定需要忽略的异常类型(即这些异常不会触发 fallback)
     * 注意:exceptionsToTrace 和 exceptionsToIgnore 不应同时使用;
     * 若同时存在,exceptionsToIgnore 优先级更高
     */
    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}

实际使用示例

下面是一个完整的使用示例,展示了 @SentinelResource 注解的各种配置方式:

@RestController
public class SentinelController {

    @Autowired
    private ISentinelService service;

    @GetMapping(value = "/hello/{s}")
    public String apiHello(@PathVariable long s) {
        return service.hello(s);
    }
}

public interface ISentinelService {
    String hello(long s);
}

@Service
@Slf4j
public class SentinelServiceImpl implements ISentinelService {

    /**
     * Sentinel 提供了 @SentinelResource 注解用于定义资源
     *
     * @param s 输入参数
     * @return 返回结果
     */
    @Override
    // value:资源名称,必需项(不能为空)
    // blockHandler:对应处理 BlockException 的函数名称
    // fallback:用于在抛出异常的时候提供 fallback 处理逻辑
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
    public String hello(long s) {
        log.error("hello:{}", s);
        return String.format("Hello at %d", s);
    }

    /**
     * Fallback 函数
     * 函数签名与原函数一致,或加一个 Throwable 类型的参数
     */
    public String helloFallback(long s) {
        log.error("helloFallback:{}", s);
        return String.format("Halooooo %d", s);
    }

    /**
     * Block 异常处理函数
     * 参数最后多一个 BlockException,其余与原函数一致
     */
    public String exceptionHandler(long s, BlockException ex) {
        // Do some log here.
        log.error("exceptionHandler:{}", s);
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }
}

SentinelResourceAspect 核心逻辑

@SentinelResource 注解由 SentinelResourceAspect 切面处理,核心逻辑如下:

@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        // 获取目标方法
        Method originMethod = resolveMethod(pjp);

        // 获取注解信息
        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }

        // 获取资源配置信息
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        int resourceType = annotation.resourceType();

        Entry entry = null;
        try {
            // 创建限流入口
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            // 执行原方法
            Object result = pjp.proceed();
            return result;

        } catch (BlockException ex) {
            // 处理被限流异常
            return handleBlockException(pjp, annotation, ex);

        } catch (Throwable ex) {
            // 处理业务异常
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // 优先检查忽略列表
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            // 检查异常是否在追踪列表中
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                // 执行 fallback 逻辑
                return handleFallback(pjp, annotation, ex);
            }

            // 没有 fallback 函数可以处理该异常,直接抛出
            throw ex;

        } finally {
            // 释放 Entry 资源
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
        }
    }

    /**
     * 处理 BlockException
     *
     * blockHandler / blockHandlerClass 说明:
     * - blockHandler:对应处理 BlockException 的函数名称,可选项
     * - blockHandler 函数签名:与原方法相匹配并且最后加一个额外的参数,类型为 BlockException
     * - blockHandler 函数默认需要和原方法在同一个类中
     * - 若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象
     * - 注意:blockHandlerClass 中对应的函数必须为 static 函数,否则无法解析
     */
    protected Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
            throws Throwable {

        // 执行 blockHandler 方法(如果配置了的话)
        Method blockHandlerMethod = extractBlockHandlerMethod(pjp, annotation.blockHandler(),
                annotation.blockHandlerClass());

        if (blockHandlerMethod != null) {
            Object[] originArgs = pjp.getArgs();
            // 构造参数:原方法参数 + BlockException
            Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
            args[args.length - 1] = ex;

            try {
                // 根据 static 方法与否进行不同的调用
                if (isStatic(blockHandlerMethod)) {
                    return blockHandlerMethod.invoke(null, args);
                }
                return blockHandlerMethod.invoke(pjp.getTarget(), args);
            } catch (InvocationTargetException e) {
                // 抛出实际的异常
                throw e.getTargetException();
            }
        }

        // 如果没有 blockHandler,则尝试执行 fallback
        return handleFallback(pjp, annotation, ex);
    }

    /**
     * 处理 Fallback 逻辑
     *
     * fallback / fallbackClass 说明:
     * - fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑
     * - fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理
     *
     * fallback 函数签名和位置要求:
     * - 返回值类型必须与原函数返回值类型一致
     * - 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常
     * - fallback 函数默认需要和原方法在同一个类中
     * - 若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象
     * - 注意:fallbackClass 中对应的函数必须为 static 函数,否则无法解析
     */
    protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, String defaultFallback,
                                    Class<?>[] fallbackClass, Throwable ex) throws Throwable {
        Object[] originArgs = pjp.getArgs();

        // 执行 fallback 函数(如果配置了的话)
        Method fallbackMethod = extractFallbackMethod(pjp, fallback, fallbackClass);

        if (fallbackMethod != null) {
            // 构造参数:根据 fallback 方法的参数数量决定是否添加异常参数
            int paramCount = fallbackMethod.getParameterTypes().length;
            Object[] args;
            if (paramCount == originArgs.length) {
                args = originArgs;
            } else {
                args = Arrays.copyOf(originArgs, originArgs.length + 1);
                args[args.length - 1] = ex;
            }

            try {
                // 根据 static 方法与否进行不同的调用
                if (isStatic(fallbackMethod)) {
                    return fallbackMethod.invoke(null, args);
                }
                return fallbackMethod.invoke(pjp.getTarget(), args);
            } catch (InvocationTargetException e) {
                // 抛出实际的异常
                throw e.getTargetException();
            }
        }

        // 如果没有 fallback,尝试使用 defaultFallback
        return handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex);
    }
}

五、流控处理核心逻辑

从入口函数开始,我们深入到流控处理的核心逻辑。

入口函数调用链

public class SphU {

    /**
     * 创建限流入口
     *
     * @param name 资源名称
     * @param resourceType 资源类型
     * @param trafficType 流量类型(IN 或 OUT)
     * @param args 参数数组
     * @return Entry 对象
     * @throws BlockException 如果被限流则抛出此异常
     */
    public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args)
            throws BlockException {
        return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
    }

    public static Entry entry(String name, EntryType trafficType, int batchCount) throws BlockException {
        return Env.sph.entry(name, trafficType, batchCount, OBJECTS0);
    }
}
public class CtSph implements Sph {

    @Override
    public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
        StringResourceWrapper resource = new StringResourceWrapper(name, type);
        return entry(resource, count, args);
    }

    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        return entryWithPriority(resourceWrapper, count, false, args);
    }

    /**
     * 带优先级的入口方法,这是限流的核心逻辑
     */
    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
            throws BlockException {
        Context context = ContextUtil.getContext();

        // 如果上下文数量超过阈值,则不进行规则检查
        if (context instanceof NullContext) {
            // NullContext 表示上下文数量超过了阈值,这里只初始化 Entry,不进行规则检查
            return new CtEntry(resourceWrapper, null, context);
        }

        // 如果没有上下文,使用默认上下文
        if (context == null) {
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }

        // 如果全局开关关闭,则不进行规则检查
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }

        // 获取或创建 ProcessorSlotChain(责任链)
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

        /*
         * 如果资源(slot chain)数量超过 {@link Constants.MAX_SLOT_CHAIN_SIZE},
         * 则不进行规则检查
         */
        if (chain == null) {
            return new CtEntry(resourceWrapper, null, context);
        }

        // 创建 Entry 对象
        Entry e = new CtEntry(resourceWrapper, chain, context);

        try {
            // 执行责任链进行规则检查
            chain.entry(context, resourceWrapper, null, count, prioritized, args);
        } catch (BlockException e1) {
            // 如果被限流,释放 Entry 并抛出异常
            e.exit(count, args);
            throw e1;
        } catch (Throwable e1) {
            // 这不应该发生,除非 Sentinel 内部存在错误
            log.warn("Sentinel unexpected exception,{}", e1.getMessage());
        }
        return e;
    }
}

ProcessorSlotChain 功能插槽链

lookProcessChain 方法实际创建了 ProcessorSlotChain 功能插槽链。ProcessorSlotChain 采用责任链模式,将不同的功能(限流、降级、系统保护)组合在一起。

SlotChain 的获取与创建

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 先从缓存中获取
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);

    if (chain == null) {
        // 双重检查锁,保证线程安全
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry 大小限制
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }

                // 创建新的 SlotChain
                chain = SlotChainProvider.newSlotChain();

                // 使用不可变模式更新缓存
                Map<ResourceWrapper, ProcessorSlotChain> newMap =
                    new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}

SlotChain 的构建

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        // 通过 SPI 加载所有 ProcessorSlot 并排序
        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);

        for (ProcessorSlot slot : sortedSlotList) {
            // 只处理继承自 AbstractLinkedProcessorSlot 的 Slot
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() +
                    ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }

            // 将 Slot 添加到责任链尾部
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

SlotChain 的功能划分

Slot Chain 可以分为两部分:

  • 统计数据构建部分(statistic):负责收集各种指标数据;
  • 判断部分(rule checking):根据规则判断是否限流。

官方架构图很好地解释了各个 Slot 的作用及其负责的部分。目前 ProcessorSlotChain 的设计是一个资源对应一个,构建好后缓存起来,方便下次直接取用。

各 Slot 的执行顺序

以下是 Sentinel 中各个 Slot 的默认执行顺序:

NodeSelectorSlot
    ↓
ClusterBuilderSlot
    ↓
StatisticSlot
    ↓
ParamFlowSlot
    ↓
SystemSlot
    ↓
AuthoritySlot
    ↓
FlowSlot
    ↓
DegradeSlot

NodeSelectorSlot - 上下文节点选择

这个功能插槽主要为资源下不同的上下文创建对应的 DefaultNode(实际用于统计指标信息)。解释一下Sentinel中的Node是什么,简单来说就是每个资源统计指标存放的容器,只不过内部由于不同的统计口径(秒级、分钟及)而分别有不同的统计窗口。Node在Sentinel不是单一的结构,而是总体上形成父子关系的树形结构。

不同的调用会有不同的 context 名称,如在当前 MVC 场景下,上下文为 sentinel_web_servlet_context。

public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

    /**
     * 同一个资源在不同上下文中的 DefaultNode 映射
     */
    private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 从映射表中获取当前上下文对应的节点
        DefaultNode node = map.get(context.getName());

        if (node == null) {
            // 双重检查锁,保证线程安全
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    // 创建新的 DefaultNode
                    node = new DefaultNode(resourceWrapper, null);

                    // 使用写时复制更新缓存
                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;

                    // 构建调用树
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }
            }
        }

        // 设置当前上下文的当前节点
        context.setCurNode(node);
        // 继续执行后续 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }
}

ClusterBuilderSlot - 集群节点构建

这个功能槽主要用于创建 ClusterNode。ClusterNode 和 DefaultNode 的区别是:

DefaultNode 是特定于上下文的(context-specific);

ClusterNode 是不区分上下文的(context-independent),用于统计该资源在所有上下文中的整体数据。

public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    /**
     * 全局 ClusterNode 映射表
     */
    private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

    private static final Object lock = new Object();

    private volatile ClusterNode clusterNode = null;

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 创建 ClusterNode(如果不存在)
        if (clusterNode == null) {
            synchronized (lock) {
                if (clusterNode == null) {
                    // 创建集群节点
                    clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());

                    // 更新全局映射表
                    HashMap<ResourceWrapper, ClusterNode> newMap =
                        new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }

        // 将 ClusterNode 设置到 DefaultNode 中
        node.setClusterNode(clusterNode);

        // 如果有来源标识,则创建 origin node
        if (!"".equals(context.getOrigin())) {
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

        // 继续执行后续 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
}

StatisticSlot - 统计插槽

StatisticSlot 是 Sentinel 最重要的类之一,用于根据规则判断结果进行相应的统计操作。

统计逻辑说明

entry 的时候:

依次执行后续的判断 Slot;

每个 Slot 触发流控会抛出异常(BlockException 的子类);

若有 BlockException 抛出,则记录 block 数据;

若无异常抛出则算作可通过(pass),记录 pass 数据。

exit 的时候:

若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数 -1。

记录数据的维度:

线程数 +1;

记录当前 DefaultNode 数据;

记录对应的 originNode 数据(若存在 origin);

累计 IN 统计数据(若流量类型为 IN)。

public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // 此位置会调用 SlotChain 中后续的所有 Slot,完成所有规则检测
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // 请求通过,增加线程数和通过数
            // 代码运行到这个位置,就证明之前的所有 Slot 检测都通过了
            // 此时就可以统计请求的相应数据了

            // 增加线程数(+1)
            node.increaseThreadNum();
            // 增加通过请求的数量(这里涉及到滑动窗口算法)
            node.addPassRequest(count);

            // 省略其他统计逻辑...

        } catch (PriorityWaitException ex) {
            // 如果是优先级等待异常,记录优先级等待数
            node.increaseThreadNum();
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();
            }
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // 记录入站统计数据
                Constants.ENTRY_NODE.increaseThreadNum();
            }
            throw ex;

        } catch (BlockException e) {
            // 如果被限流,记录被限流数
            // 省略 block 统计逻辑...
            throw e;

        } catch (Throwable ex) {
            // 如果发生业务异常,记录异常数
            // 省略异常统计逻辑...
            throw ex;
        }
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        // 若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数-1
        // 记录数据的维度:线程数+1、记录当前 DefaultNode 数据、记录对应的 originNode 数据(若存在 origin)
        // 、累计 IN 统计数据(若流量类型为 IN)
        // 省略 exit 统计逻辑...
    }
}

StatisticNode 数据结构

到这里,StatisticSlot 的作用已经比较清晰了。接下来我们需要分析它的统计数据结构。fireEntry 调用向下的节点和之前的方式一样,剩下的节点主要包括:

  • ParamFlowSlot;
  • SystemSlot;
  • AuthoritySlot;
  • FlowSlot;
  • DegradeSlot;

其中比较常见的是流控和熔断:FlowSlot、DegradeSlot,所以下面我们着重分析 FlowSlot。

六、FlowSlot - 流控插槽

这个 Slot 主要根据预设的资源的统计信息,按照固定的次序依次生效。如果一个资源对应两条或者多条流控规则,则会根据如下次序依次检验,直到全部通过或者有一个规则生效为止。

FlowSlot 核心逻辑

@SpiOrder(-2000)
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 执行流控检查
        checkFlow(resourceWrapper, context, node, count, prioritized);

        // 继续执行后续 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    // 省略其他方法...
}

checkFlow 方法详解

/**
 * 执行流控检查
 *
 * @param ruleProvider 规则提供者函数
 * @param resource 资源包装器
 * @param context 上下文
 * @param node 节点
 * @param count 请求数量
 * @param prioritized 是否优先
 * @throws BlockException 如果被限流则抛出异常
 */
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                      Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    // 判断规则和资源不能为空
    if (ruleProvider == null || resource == null) {
        return;
    }

    // 获取指定资源的所有流控规则
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());

    // 逐个应用流控规则。若无法通过则抛出异常,后续规则不再应用
    if (rules != null) {
        for (FlowRule rule : rules) {
            if (!canPassCheck(rule, context, node, count, prioritized)) {
                // FlowException 继承 BlockException
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }
    }
}

通过这里我们就可以得知,流控规则是通过 FlowRule 来完成的,数据来源是我们使用的流控控制台,也可以通过代码进行设置。

FlowRule 流控规则

每条流控规则主要由三个要素构成:

  • grade(阈值类型):按 QPS(每秒请求数)还是线程数进行限流;
  • strategy(调用关系策略):基于调用关系的流控策略;
  • controlBehavior(流控效果):当 QPS 超过阈值时的流量整形行为。
public class FlowRule extends AbstractRule {

    public FlowRule() {
        super();
        // 来源默认 Default
        setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
    }

    public FlowRule(String resourceName) {
        super();
        // 资源名称
        setResource(resourceName);
        setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
    }

    /**
     * 流控的阈值类型
     * 0: 线程数
     * 1: QPS
     */
    private int grade = RuleConstant.FLOW_GRADE_QPS;

    /**
     * 流控阈值
     */
    private double count;

    /**
     * 基于调用链的流控策略
     * STRATEGY_DIRECT: 直接流控(按来源)
     * STRATEGY_RELATE: 关联流控(关联资源)
     * STRATEGY_CHAIN: 链路流控(按入口资源)
     */
    private int strategy = RuleConstant.STRATEGY_DIRECT;

    /**
     * 关联流控模式下的关联资源
     */
    private String refResource;

    /**
     * 流控效果(流量整形行为)
     * 0: 默认(直接拒绝)
     * 1: 预热(Warm Up)
     * 2: 排队等待(Rate Limiter)
     * 3: 预热 + 排队等待(目前控制台没有)
     */
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;

    /**
     * 预热时长(秒)
     */
    private int warmUpPeriodSec = 10;

    /**
     * 排队等待的最大超时时间(毫秒)
     */
    private int maxQueueingTimeMs = 500;

    /**
     * 是否为集群模式
     */
    private boolean clusterMode;

    /**
     * 集群模式配置
     */
    private ClusterFlowConfig clusterConfig;

    /**
     * 流量整形控制器
     */
    private TrafficShapingController controller;

    // 省略 getter/setter 方法...
}

七、滑动窗口算法

不管流控规则采用何种流控算法,在底层都需要有支持指标统计的数据结构作为支撑。在 Sentinel 中,用于支撑基于 QPS 等限流的数据结构是 StatisticNode。

StatisticNode 数据结构

public class StatisticNode implements Node {

    /**
     * 保存最近 1 秒内的统计数据
     * 每个桶(bucket)500ms,共 2 个桶
     */
    private transient volatile Metric rollingCounterInSecond =
        new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);

    /**
     * 保存最近 60 秒的统计数据
     * windowLengthInMs 被特意设置为 1000 毫秒,即每个桶代表 1 秒
     * 共 60 个桶,这样可以获得每秒精确的统计信息
     */
    private transient Metric rollingCounterInMinute =
        new ArrayMetric(60, 60 * 1000, false);

    // 省略其他字段和方法...
}

ArrayMetric 核心实现

ArrayMetric 是 Sentinel 中数据采集的核心,内部使用了 BucketLeapArray,即滑动窗口的思想进行数据的采集。

public class ArrayMetric implements Metric {

    /**
     * 滑动窗口数组
     */
    private final LeapArray<MetricBucket> data;

    public ArrayMetric(int sampleCount, int intervalInMs) {
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
        if (enableOccupy) {
            // 可抢占的滑动窗口,支持借用未来窗口的配额
            this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
        } else {
            // 普通滑动窗口
            this.data = new BucketLeapArray(sampleCount, intervalInMs);
        }
    }
}

这里有两种实现:

  • BucketLeapArray:普通滑动窗口,每个时间桶仅记录固定时间窗口内的指标数据;
  • OccupiableBucketLeapArray:扩展实现,支持"抢占"未来时间窗口的令牌或容量,在流量突发时允许借用后续窗口的配额,实现更平滑的限流效果。

BucketLeapArray - 滑动窗口实现

LeapArray 核心属性

LeapArray 是滑动窗口的基础类,其核心属性如下:

/**
 * 窗口大小(长度),单位:毫秒
 * 例如:1000ms
 */
private int windowLengthInMs;

/**
 * 样本数(桶的数量)
 * 例如:5(表示 5 个桶,每个 1000ms,总共 5 秒)
 */
private int sampleCount;

/**
 * 采集周期(总时间窗口长度),单位:毫秒
 * 例如:5 * 1000ms(5 秒)
 */
private int intervalInMs;

/**
 * 窗口数组,array 长度就是样本数 sampleCount
 */
protected final AtomicReferenceArray<WindowWrap<T>> array;

/**
 * 更新窗口数据的锁,保证数据的正确性
 */
private final ReentrantLock updateLock;

WindowWrap 窗口包装器

每个窗口包装器包含三个属性:

 public class WindowWrap<T> {

    /**
     * 窗口大小(长度),单位:毫秒
     * 与 LeapArray 中的 windowLengthInMs 一致
     */
    private final long windowLengthInMs;

    /**
     * 窗口开始时间戳
     * 它的值是 windowLengthInMs 的整数倍
     */
    private long windowStart;

    /**
     * 窗口数据(泛型 T)
     * Sentinel 目前只有 MetricBucket 类型,存储统计数据
     */
    private T value;
}

MetricBucket 指标桶

public class MetricBucket {

    /**
     * 计数器数组
     * 长度是需要统计的事件种类数,目前是 6 个
     * LongAdder 是线程安全的计数器,性能优于 AtomicLong
     */
    private final LongAdder[] counters;
    
    // 省略其他字段和方法...
}

滑动窗口工作原理

LeapArray 统计数据的基本思路:

创建一个长度为 n 的数组,数组元素就是窗口;

每个窗口包装了 1 个指标桶,桶中存放了该窗口时间范围内对应的请求统计数据;

可以想象成一个环形数组在时间轴上向右滚动;

请求到达时,会命中数组中的一个窗口,该请求的数据就会存到命中的这个窗口包含的指标桶中;

当数组转满一圈时,会回到数组的开头;

此时下标为 0 的元素需要重复使用,它里面的窗口数据过期了,需要重置,然后再使用。

获取当前窗口

LeapArray 获取当前时间窗口的方法:

 /**
 * 获取当前时间戳对应的窗口
 *
 * @return 当前时间的窗口
 */
public WindowWrap<T> currentWindow() {
    return currentWindow(TimeUtil.currentTimeMillis());
}

/**
 * 获取指定时间戳对应的窗口(核心方法)
 *
 * @param timeMillis 时间戳(毫秒)
 * @return 对应的窗口
 */
public WindowWrap<T> currentWindow(long timeMillis) {
    if (timeMillis < 0) {
        return null;
    }

    // 计算数组下标
    int idx = calculateTimeIdx(timeMillis);

    // 计算当前请求对应的窗口开始时间
    long windowStart = calculateWindowStart(timeMillis);

    // 无限循环,确保能够获取到窗口
    while (true) {
        // 取窗口
        WindowWrap<T> old = array.get(idx);

        if (old == null) {
            // 第一次使用,创建新窗口
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));

            // CAS 操作,确保只初始化一次
            if (array.compareAndSet(idx, null, window)) {
                // 成功更新,返回创建的窗口
                return window;
            } else {
                // CAS 失败,让出时间片,等待其他线程完成初始化
                Thread.yield();
            }

        } else if (windowStart == old.windowStart()) {
            // 命中:取出的窗口的开始时间和本次请求计算出的窗口开始时间一致
            return old;

        } else if (windowStart > old.windowStart()) {
            // 窗口过期:本次请求计算出的窗口开始时间大于取出的窗口
            // 说明取出的窗口过期了,需要重置
            if (updateLock.tryLock()) {
                try {
                    // 成功获取锁,更新窗口开始时间,计数器重置
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                // 获取锁失败,让出时间片,等待其他线程更新
                Thread.yield();
            }

        } else if (windowStart < old.windowStart()) {
            // 异常情况:机器时钟回拨等
            // 正常情况不会进入该分支
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

数据存储

在获取到窗口之后,就可以存储数据了。ArrayMetric 实现了 Metric 中存取数据的接口方法。

示例:存储 RT(响应时间)

/**
 * 添加响应时间数据
 *
 * @param rt 响应时间(毫秒)
 */
public void addRT(long rt) {
    // 获取当前时间窗口,data 为 BucketLeapArray
    WindowWrap<MetricBucket> wrap = data.currentWindow();

    // 计数
    wrap.value().addRT(rt);
}

/**
 * MetricBucket 的 addRT 方法
 *
 * @param rt 响应时间
 */
public void addRT(long rt) {
    // 记录 RT 时间对 rt 值
    add(MetricEvent.RT, rt);

    // 记录最小响应时间(非线程安全,但没关系)
    if (rt < minRt) {
        minRt = rt;
    }
}

/**
 * 通用的计数方法
 *
 * @param event 事件类型
 * @param n 增加的数量
 * @return 当前桶
 */
public MetricBucket add(MetricEvent event, long n) {
    counters[event.ordinal()].add(n);
    return this;
}

数据读取

示例:读取 RT(响应时间)

/**
 * 获取总响应时间
 *
 * @return 总响应时间
 */
public long rt() {
    // 触发当前窗口更新(处理过期窗口)
    data.currentWindow();

    long rt = 0;
    // 取出所有的 bucket
    List<MetricBucket> list = data.values();

    for (MetricBucket window : list) {
        rt += window.rt(); // 求和
    }
    return rt;
}

/**
 * 获取所有有效的窗口
 *
 * @return 有效窗口列表
 */
public List<T> values() {
    return values(TimeUtil.currentTimeMillis());
}

/**
 * 获取指定时间之前的所有有效窗口
 *
 * @param timeMillis 时间戳
 * @return 有效窗口列表
 */
public List<T> values(long timeMillis) {
    if (timeMillis < 0) {
        return new ArrayList<T>(); // 正常情况不会到这里
    }

    int size = array.length();
    List<T> result = new ArrayList<T>(size);

    for (int i = 0; i < size; i++) {
        WindowWrap<T> windowWrap = array.get(i);

        // 过滤掉没有初始化过的窗口和过期的窗口
        if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
            continue;
        }

        result.add(windowWrap.value());
    }
    return result;
}

/**
 * 判断窗口是否过期
 *
 * @param time 给定时间(通常是当前时间)
 * @param windowWrap 窗口包装器
 * @return 如果过期返回 true
 */
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
    // 给定时间与窗口开始时间超过了一个采集周期
    return time - windowWrap.windowStart() > intervalInMs;
}

OccupiableBucketLeapArray - 可抢占窗口

为什么需要 OccupiableBucketLeapArray?

假设一个资源的访问 QPS 稳定是 10,请求是均匀分布的:

在时间 0.0-1.0 秒区间中,通过了 10 个请求;

在 1.1 秒的时候,观察到的 QPS 可能只有 5,因为此时第一个时间窗口被重置了,只有第二个时间窗口有值;

当在秒级统计的情形下,用 BucketLeapArray 会有 0~50%的数据误这时就要用 OccupiableBucketLeapArray 来解决这个问题。

OccupiableBucketLeapArray 实现

从上面我们可以看到在秒级统计 rollingCounterInSecond 中,初始化实例时有两种构造参数:

public class OccupiableBucketLeapArray extends LeapArray<MetricBucket> {

    /**
     * 借用未来窗口的数组
     */
    private final FutureBucketLeapArray borrowArray;

    public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
        super(sampleCount, intervalInMs);
        // 创建借用窗口数组
        this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
    }

    /**
     * 创建新的空桶
     * 会从 borrowArray 中借用数据
     */
    @Override
    public MetricBucket newEmptyBucket(long time) {
        MetricBucket newBucket = new MetricBucket();

        // 获取借用窗口的数据
        MetricBucket borrowBucket = borrowArray.getWindowValue(time);
        if (borrowBucket != null) {
            // 将借用数据复制到新桶中
            newBucket.reset(borrowBucket);
        }

        return newBucket;
    }

    /**
     * 重置窗口
     * 会从 borrowArray 中借用 pass 数据
     */
    @Override
    protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) {
        // 更新开始时间并重置值
        w.resetTo(time);

        MetricBucket borrowBucket = borrowArray.getWindowValue(time);
        if (borrowBucket != null) {
            // 重置桶值并添加借用的 pass 数据
            w.value().reset();
            w.value().addPass((int) borrowBucket.pass());
        } else {
            w.value().reset();
        }

        return w;
    }

    /**
     * 获取当前等待中的请求数量
     */
    @Override
    public long currentWaiting() {
        borrowArray.currentWindow();
        long currentWaiting = 0;
        List<MetricBucket> list = borrowArray.values();

        for (MetricBucket window : list) {
            currentWaiting += window.pass();
        }
        return currentWaiting;
    }

    /**
     * 添加等待中的请求数量
     *
     * @param time 时间
     * @param acquireCount 获取数量
     */
    @Override
    public void addWaiting(long time, int acquireCount) {
        WindowWrap<MetricBucket> window = borrowArray.currentWindow(time);
        window.value().add(MetricEvent.PASS, acquireCount);
    }
}

八、总结

至此,Sentinel 的基本情况都已经分析完成。以上内容主要讲解了 Sentinel 的核心处理流程,包括:

核心流程总结

  1. 规则加载:
  • 通过 RuleLoader 将各种规则(流控、熔断、系统限流等)加载到内存缓存中。
  1. 请求拦截:
  • 通过 SentinelServletFilter 过滤器拦截 HTTP 请求;
  • 通过SentinelResourceAspect切面处理 @SentinelResource 注解。
  1. 责任链处理:
  • 使用 ProcessorSlotChain 责任链模式组合多个功能插槽;
  • 每个插槽负责特定的功能(统计、流控、熔断等)。
  1. 流控判断:
  • FlowSlot 根据流控规则判断是否限流;
  • 通过滑动窗口算法统计 QPS、线程数等指标。
  1. 异常处理:
  • 被限流时抛出 BlockException;
  • 通过 blockHandler 或 fallback 处理异常。

核心技术点

  1. 责任链模式:
  • 通过 ProcessorSlotChain 将不同的限流功能组合在一起。
  1. 滑动窗口算法:
  • LeapArray 实现环形滑动窗口;
  • BucketLeapArray 普通滑动窗口;
  • OccupiableBucketLeapArray 可抢占窗口,支持借用未来配额。
  1. 数据结构:
  • DefaultNode:特定于上下文的统计节点;
  • ClusterNode:不区分上下文的集群统计节点;
  • StatisticNode:核心统计节点,包含秒级和分钟级统计。
  1. 限流算法:
  • QPS 限流:通过滑动窗口统计 QPS;
  • 线程数限流:通过原子计数器统计线程数;
  • 流控效果:快速失败、预热、排队等待等;

Sentinel 通过精心设计的架构,实现了高效、灵活、可扩展的流量控制能力,为微服务系统提供了强大的保护机制。

往期回顾

1.社区推荐重排技术:双阶段框架的实践与演进|得物技术

2.Flink ClickHouse Sink:生产级高可用写入方案|得物技术

3.服务拆分之旅:测试过程全揭秘|得物技术

4.大模型网关:大模型时代的智能交通枢纽|得物技术

5.从“人治”到“机治”:得物离线数仓发布流水线质量门禁实践

文 /万钧

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

什么是泛域名证书?

泛域名证书,又称通配符SSL证书,是一种特殊的数字证书。它通过在域名前加上星号(*.)来表示,可以同时保护一个主域名及该主域名下所有的同一级子域名 。

核心特性与规则:

  • 一证多用:一张证书即可保护无数个同级子域名。例如购买 *.example.com 的证书,它可以同时用于 www.example.commail.example.comblog.example.com 等,未来新增子域名也无需重复申请。
  • 同级限制:需要注意的是,泛域名证书仅支持一级子域名。它无法保 护 test.forum.example.com 这样的二级或多级子域名 。
  • 简化管理:只需部署一次,即可让所有子域名继承“信任”,大大降低了证书续期和配置的运维工作量。

如何申请泛域名证书?(以JoySSL为例)

虽然阿里云、华为云等厂商均提供付费泛域名证书,但对于个人站长或中小企业来说,JoySSL 提供的永久免费泛域名证书是一个极具性价比的选择。它拥有全中文界面,且支持无限续签,非常适合国内用户 。

以下是具体的申请步骤:泛域名证书申请入口

1. 注册账号并选择产品

访问 JoySSL 官方网站,注册一个新账号。为了提高注册成功率,建议填写真实有效的邮箱及联系人信息。

  • 注册码:在注册过程中,活动页面要求填写注册码。可填写官方指定的 230970 或关注其官网最新公告获取,以确保能够申请永久免费证书。
2. 申请免费通配符证书

登录后,在证书列表中找到“免费体验版”或“永久免费SSL证书”分类,选择“免费版通配符SSL证书”。

  • 提交您需要保护的主域名(如 example.com),系统会自动识别为 *.example.com 的泛域名证书。
3. 域名所有权验证(DNS验证)

为了证明您拥有该域名的控制权,CA机构会要求验证。JoySSL 通常支持DNS验证(推荐)或文件验证 。

  • 操作:在订单详情中,会提供一条需要解析的TXT记录
  • 配置:您需要登录购买域名的服务商后台(如阿里云、腾讯云、GoDaddy等),在该域名的DNS解析设置中添加一条相应的TXT记录 。
  • 等待:添加完成后,回到JoySSL后台点击“验证”,通常几分钟内即可自动完成审核。
4. 下载与部署证书

验证通过后,证书会正式签发。

  • 下载证书包,根据您的服务器环境(如Nginx、Apache、IIS等)选择对应的版本。
  • 参照官方提供的安装文档,将证书文件上传至服务器并进行配置。重启Web服务后,您的网站主域名及所有子域名即可通过HTTPS安全访问。

总结

对于拥有多子域名架构的网站而言,泛域名证书不仅是成本控制的手段,更是提升管理效率的利器。通过 JoySSL 这类服务平台,你可以用极低的成本(甚至为零)快速获得由权威CA签发、浏览器信任的泛域名证书,让网站安全等级一步到位。

在移动应用分发领域,代码签名证书是确保应用合法性、完整性和用户信任的核心工具。无论是iOS的封闭生态还是Android的开放体系,未签名或签名不当的应用均可能被系统拦截,导致安装失败或触发安全警告。本文从技术原理、平台差异、常见误区及解决方案四个维度,系统梳理iOS/Android代码签名证书的避坑要点,并分析JoySSL在证书管理中的技术优势。

一、代码签名证书的核心价值与技术原理

1. 身份认证与代码完整性保护

代码签名证书通过非对称加密技术(如RSA 2048/4096或国密SM2算法)生成公私钥对,开发者使用私钥对应用代码进行签名,操作系统或应用商店通过公钥验证签名有效性。这一过程可实现两大核心功能:

  • 身份溯源:用户可通过证书信息确认应用开发者身份,避免“未知来源”警告;
  • 防篡改:任何代码修改均会导致签名失效,阻断中间人攻击或恶意代码注入。

2. 信任链构建机制

以iOS为例,其签名体系包含三层结构:

  • 开发者证书:由苹果CA颁发,绑定开发者账号;
  • 描述文件(Provisioning Profile) :关联证书、App ID和设备列表,控制应用分发范围;
  • 应用签名:使用证书私钥对编译后的IPA文件签名,生成唯一指纹。

Android的签名机制虽更开放,但同样依赖证书建立信任链。自Android 7.0起,系统强制要求APK签名使用V2/V3方案,通过全文件哈希校验提升安全性。

二、iOS代码签名:封闭生态的严格规范与常见陷阱

1. 证书类型与适用场景

iOS提供两类开发者证书:

  • 开发证书(Development Certificate) :用于调试阶段,绑定特定设备UDID,有效期1年;
  • 发布证书(Distribution Certificate) :用于App Store分发或企业内部分发,有效期3年。

避坑要点

  • 证书滥用:企业证书仅限内部使用,若用于分发非企业应用,可能被苹果吊销;
  • 设备数量限制:个人开发者账号最多绑定100台设备,超限需清理旧设备或升级账号类型;
  • Bundle ID冲突:App ID需与项目中的Bundle Identifier完全一致,否则导致签名失败。

2. 描述文件管理误区

描述文件是iOS签名的核心配置文件,其常见问题包括:

  • 过期未更新:描述文件有效期与证书绑定,需同步续期;
  • 权限遗漏:若应用需使用摄像头、相册等权限,需在描述文件中声明用途(如NSCameraUsageDescription);
  • 多环境混淆:开发、测试、发布环境需使用不同描述文件,避免交叉污染。

3. JoySSL在iOS签名中的优化实践

针对iOS证书管理痛点,JoySSL提供以下解决方案:

  • 自动化工具链:集成AppUploader等工具,支持在Windows/Linux环境生成证书和描述文件,减少对macOS的依赖;
  • 证书监控预警:通过自主监控平台提前30天预警证书到期,避免因过期导致应用下架;
  • 多账号管理:支持企业级账号体系,实现证书、描述文件和设备的集中化管控。

三、Android代码签名:开放生态的灵活性与安全挑战

1. 签名方案演进与兼容性

Android支持三种签名方案:

  • V1签名(JAR签名) :基于ZIP文件结构校验,易被破解;
  • V2签名(APK签名方案v2) :对APK进行全文件哈希校验,提升安全性;
  • V3签名(APK签名方案v3) :引入签名块旋转机制,支持签名信息动态更新。

避坑要点

  • 强制V2/V3签名:Android 7.0+设备默认要求V2签名,若未启用可能导致安装失败;
  • 多渠道包冲突:使用Gradle多渠道打包时,需确保所有渠道包使用同一证书签名,否则无法覆盖安装;
  • 密钥泄露风险:私钥丢失将导致应用无法更新,需立即吊销证书并重新签名。

2. 应用备案与签名信息绑定

根据中国工信部要求,Android应用需完成备案方可上架,备案信息包含:

  • 包名(Package Name) :全局唯一标识符(如com.example.app);
  • 公钥指纹:签名证书的SHA-256或MD5值;
  • 应用签名值:备案系统通过比对签名值防止应用被篡改。

避坑要点

  • 备案信息一致性:应用更新时需保持包名、公钥和签名值不变,否则需重新备案;
  • 签名证书轮换:若因安全原因更换证书,需同步更新备案信息,避免审核被拒。

3. JoySSL在Android签名中的技术优势

针对Android签名复杂性,JoySSL提供以下支持:

  • 多算法兼容:支持RSA、ECC和国密SM2算法,满足不同场景需求;
  • 时间戳服务:通过RFC 3161标准时间戳延长签名有效期,避免因证书过期导致旧版应用失效;
  • 自动化签名流水线:集成Jenkins、GitHub Actions等CI/CD工具,实现代码提交后自动签名和发布。

四、跨平台签名管理:统一化与自动化趋势

1. 代码签名证书的有效期新政

2026年3月起,全球代码签名证书有效期将从39个月缩短至460天(约15个月)。这一政策旨在降低证书被盗用风险,但对企业证书管理提出更高要求:

  • 续期频率增加:证书需每年更新,运维成本上升;
  • 自动化需求增长:手动续期易出错,需依赖自动化工具。

JoySSL通过自主监控平台实现证书生命周期管理,支持批量续期和自动部署,显著降低管理成本。

2. 区块链存证增强防篡改能力

部分CA机构(如JoySSL)开始探索将签名记录上链,通过分布式账本技术实现:

  • 签名信息不可篡改:每次签名操作均生成唯一哈希值并上链存储;
  • 审计追溯效率提升:监管机构可快速验证签名历史,打击恶意篡改行为。

五、总结:代码签名证书管理的核心原则

  1. 合规性优先:严格遵循苹果、谷歌及应用商店的签名规范,避免因违规导致应用下架;
  2. 密钥安全至上:采用HSM硬件或云密钥管理服务(如JoySSL的KMS)保护私钥,杜绝泄露风险;
  3. 自动化驱动效率:通过CI/CD工具链实现签名流程标准化,减少人为错误;
  4. 监控预警常态化:建立证书到期预警机制,确保业务连续性。

在移动应用竞争日益激烈的今天,代码签名证书不仅是合规门槛,更是建立用户信任、抵御安全威胁的核心基础设施。开发者需结合平台特性与技术趋势,选择权威CA机构(如JoySSL)的解决方案,以实现安全与效率的平衡。

作者:林润骑(太业)

背景

在多云战略日益普及的今天,企业往往需要在不同云平台部署业务系统,同时又希望将可观测数据统一采集到单一平台进行分析和管理。然而,跨云数据传输的高昂成本成为了企业实施统一可观测性战略的主要障碍。

我们发现,CloudFront 等 CDN 产品的出站流量价格更低,随用量增加还享有阶梯折扣。通过将 CDN 作为数据传输的“跳板”,可以大幅降低跨云传输成本。

基于这一发现,阿里云可观测团队设计了 LoongCollector + CDN 的跨云低成本采集方案:

  • LoongCollector 作为新一代可观测数据采集器,具备 10 倍于同类开源方案的吞吐性能,同时资源占用降低 50% 以上,确保数据链路的高效与稳定。
  • CDN 作为流量出口,利用其价格优势和全球加速能力,在保证传输质量的同时显著降低成本。

本方案可将跨云数据传输成本显著降低,让企业以更低的代价实现统一可观测平台的愿景。

现有方案 & 痛点

image

方案一:纯公网

日志服务 SLS 对外提供公网域名,用户可以直接通过公网将数据发送到 SLS,且 SLS 不收取入站流量费用。

痛点

  • 成本问题:虽然 SLS 不收取入站流量费用,但跨云采集面临源云平台的出站流量费用。以 AWS 为例,数据传输到互联网的费用约为 $0.09/GB,对于大规模数据采集场景,成本不可忽视。
  • 网络质量问题:跨云公网访问受网络波动影响较大,可能出现丢包、延迟增加等问题,影响数据采集的稳定性和实时性。

方案二:纯公网 + SLS 加速域名

SLS 传输加速利用全球分布的云机房,将全球各地用户对日志服务的访问,经过智能路由解析至就近的接入点,使用优化后的网络及协议极大地提升访问速度。

痛点

双重成本:除了源云平台的出站流量费用之外,还需要承担 DCDN 的加速费用,整体成本进一步增加。

方案三:跨云专线打通

通过云服务提供商的专线服务(如 AWS Direct Connect、阿里云高速通道等)建立跨云专用网络连接。

痛点

  • 建设成本高:专线建设需要一次性投入大量资金,包括端口费用、专线租用费用等。
  • 维护复杂:需要专业团队维护专线连接,运维成本高。
  • 灵活性差:专线带宽固定,难以应对突发流量需求。
  • 建设周期长:从申请到开通通常需要数周甚至数月时间。

跨云低成本采集方案

EC2 每月有 100GB 的出站免费额度,但是 CloudFront 的出站和公网访问源站的价格要比 EC2 的价格更低。更重要的是,CDN 产品通常提供分级定价和批量折扣,随着使用量增加,单位成本会进一步降低。通过 CDN 的加速链路,将 SLS 服务设置成源站的方式,我们可以复用 CDN 的转发链路,实现以下优势:

  • 成本优化:利用 CDN 的价格优势,降低数据传输成本。
  • 易于实施:无需建设专线,配置简单,可快速上线。
  • 弹性扩展:按需使用,无需预留带宽,可灵活应对流量波动。

CloudFront 区域数据传输到源站的价格:

image

CloudFront 请求次数价格(每月前 100 万次免费):

image

EC2 出公网价格:

image

以美国地域为例:10TB 的数据,通过 CloudFront 传输,成本比 EC2 直接公网传输可以节约 70%。

整体方案

本方案以 CloudFront 为例,整体的采集方案如图所示:

image

架构概览

AWS EC2(LoongCollector)

  • 部署在 AWS 上的采集/转发程序
  • 主要职责:

a. 从本地/应用采集日志或数据

b. 按 SLS 写入协议组包(HTTP POST)

c. 将数据发送到目标 SLS

CloudFront

  • 作为数据链路的中转入口,提供统一的域名接入点与边缘节点接入能力
  • 主要职责:

a. 接收 LoongCollector 的请求(HTTP/HTTPS)

b. 按行为规则转发到源站(此处源站为阿里云 SLS 的写入端)

SLS(阿里云日志服务)

  • 作为日志/数据接收与存储分析平台
  • 对外暴露 HTTP(S) 写入接口
  • 写入后数据落入指定 Project / Logstore(图中 Project)

SLS ConfigServer(管控端)

  • 用于下发采集配置、心跳、元数据管理、鉴权信息刷新等“控制面”能力
  • 对数据量要求低、实时性要求相对可控

链路分层:管控链路 & 数据链路

A. 管控链路(Control Plane)——直连公网

特点:请求量小、数据量小、对带宽不敏感。

  • LoongCollector 直接通过公网访问 SLS ConfigServer
  • 典型动作包括:

    • 拉取采集配置/规则
  • 选择直连公网的原因:

    • 控制流量小,对成本与链路质量要求不高
    • 架构更简单(减少中转层)

B. 数据链路(Data Plane)——通过 CloudFront 转发到 SLS

特点:持续写入、对稳定性/连通性敏感,可能存在跨境网络波动。

  • LoongCollector 将日志数据以 HTTP POST 方式发送到 CloudFront 域名
  • CloudFront 再将请求回源转发到阿里云 SLS 写入端
  • SLS 接收后写入指定 Project/Logstore

CloudFront 详细配置

这里以采集数据到 SLS 上海地域的 Project 为例。

源配置

image

注意事项:

  1. SLS 的域名不要带 Project 前缀。
  2. CloudFront 访问 SLS 域名,使用 HTTP 或者 HTTPS 都可以。

行为配置

image

image

注意事项:

  1. CDN 默认会缓存响应内容,但 LoongCollector 发送数据是 POST 请求,需要配置为不缓存。
  2. CloudFront 对 SLS 域名的请求,需要转发除 HOST 外所有的 header。

CloudFront 的域名验证

image

直接 Curl CloudFront 域名,如果有如下返回,则说明已配置成功。

{"Error":{"Code":"OLSInvalidMethod","Message":"The script name is invalid : /","RequestId":"XXX"}}

image

LoongCollector 详细配置

使用 HTTP 协议发送数据

# /usr/local/ilogtail/ilogtail_config.json
{
    "primary_region" : "cn-shanghai",
    "config_servers" :
    [
        "https://logtail.cn-shanghai.log.aliyuncs.com"
    ],
    "data_servers" :
    [
        {
            "region" : "cn-shanghai",
            "disable_subdomain" : true,
            "endpoint_list": [
                "http://xxx.cloudfront.net"
            ]
        }
    ],
    ...
}

关键配置说明:

  • config_servers 中,配置 SLS 公网域名,标准格式为 logtail.${region}.log.aliyuncs.com
  • data_servers 中
  • 只需要配置主地域,endpoint_list 中设置成 HTTP 的 CloudFront 域名
  • disable_subdomain: true(关闭子域名转发)

使用 HTTPS 协议发送数据

# /usr/local/ilogtail/ilogtail_config.json
{
    "primary_region" : "cn-shanghai",
    "config_servers" :
    [
        "https://logtail.cn-shanghai.log.aliyuncs.com"
    ],
    "data_servers" :
    [
        {
            "region" : "cn-shanghai",
            "disable_subdomain" : true,
            "endpoint_list": [
                "https://xxx.cloudfront.net"
            ]
        }
    ],
    "enable_host_ip_replace": false,
    ...
}

关键配置说明:

  • config_servers 中,配置 SLS 公网域名,标准格式为 logtail.${region}.log.aliyuncs.com
  • data_servers
  • 只需要配置主地域,endpoint_list 中设置成 HTTPS 的 CloudFront 域名
  • disable_subdomain: true(关闭子域名转发)
  • enable_host_ip_replace: false(关闭 LoongCollector 内部的 DNS 解析)

LoongCollector 资源参数配置

LoongCollector 是部署在 EC2 或者节点上,因此需要预估单机采集的日志的原始数据量,调整资源参数,具体可以参考帮助文档:https://help.aliyun.com/zh/sls/select-a-network-type

image

PS:LoongCollector 进行数据发送的时候,默认采用 LZ4 进行压缩,针对日志数据,可以有 5 ~ 10 倍的压缩效果。

网络质量测试结果

测试场景:

  • 韩国地域 EC2 采集数据到韩国地域 SLS。
  • 数据包压缩后 800K 左右。

image

可以看到在同地域场景下,CloudFront 的访问质量跟直接公网访问基本持平,HTTP 的访问延迟还是略低一些。

限制说明

  • LoongCollector 版本需要>=3.3.0。
  • 目前仅支持日志数据采集,时序、主机监控等数据还不支持通过 CDN 链路发送。
  • 本功能逐步按照地域灰度中,如需使用,可以联系 SLS 技术支持人员。

总结与展望

方案总结

本文介绍的跨云低成本可观测数据实时采集方案,通过 CDN + LoongCollector 的组合,实现了:

  1. 成本降低:相比纯公网方案,显著降低跨云数据传输成本。
  2. 性能提升:利用 CDN 的全球节点和 LoongCollector 的高性能,提升数据采集速度和稳定性。
  3. 易于实施:配置简单,无需建设专线,可快速上线。
  4. 灵活扩展:按需付费,自动弹性扩展,适应流量波动。

LoongCollector 作为新一代统一可观测 Agent,将持续致力于为用户提供高性能、低成本、易使用的跨云数据采集解决方案,助力企业构建统一的可观测平台。

参考资料:

[1] LoongCollector 官方文档

https://github.com/alibaba/loongcollector

[2] AWS CloudFront 官方文档

https://docs.aws.amazon.com/cloudfront/

[3] 阿里云 SLS 官方文档

https://help.aliyun.com/zh/sls/