普通视图

发现新文章,点击刷新页面。
昨天以前R4y的博客

我们是不是已经进入了一个”魔法时代

作者 auto
2025年12月7日 00:00

2022 年底,ChatGPT 横空出世。短短几个月内,这个能写代码、能写论文、能陪你聊天的 AI,席卷了整个互联网。

身边很多非技术圈的朋友问我:这玩意儿是怎么做到的?我尝试解释神经网络、大模型训练……他们的眼神从好奇变成茫然,最后只剩下一句:”太神奇了,简直像魔法一样。”

这让我想起英国科幻作家阿瑟·克拉克(Arthur C. Clarke)的那句:

Any sufficiently advanced technology is indistinguishable from magic.
(任何足够先进的技术,初看都与魔法无异。)

AI 时代的加速到来,让这句话从科幻预言变成了现实写照。当大多数人只知道”对话框里打字就能得到答案”,却完全不理解背后发生了什么——我们是不是已经进入了一个”魔法时代”?

魔法结界正在形成

回望人类历史,技术从来都是少数人的专利。但过去的技术壁垒,至少还是”可见”的:你不会铸剑,但你能看到铁匠挥锤;你不会织布,但你能理解经纬交错的原理。

而现在,壁垒正在变得隐形。

当你对着手机说一句话,信息穿越数千公里到达另一个人的耳边——这中间发生了什么?电磁波、基站、光纤、服务器、加密协议……对于绝大多数人来说,这些词汇和”魔法咒语”没有本质区别。

更关键的是,这种”不理解”正在成为常态,甚至被鼓励。

产品设计追求”无感”体验,用户不需要知道背后的原理,只需要享受结果。技术栈越堆越高,即便是专业开发者,也只能精通其中一小块领域。AI 的崛起更是加速了这个进程——当机器可以自己写代码、自己做决策,人类对技术的理解反而在后退。

我把这种现象称为”魔法结界”的形成:技术创造者与使用者之间,正在出现一道越来越难以跨越的认知鸿沟。

若干年后的世界

让我们大胆畅想一下,当这道结界彻底成型,世界会变成什么样?

场景一:魔法师与麻瓜

懂技术的人成为新时代的”魔法师”,他们能够调用 AI、操控数据、构建系统。而不懂技术的人则成为”麻瓜”,他们生活在魔法师构建的世界里,享受便利,却对运行规则一无所知。

这不是歧视,而是一种客观的分层。就像今天,绝大多数人不知道电是怎么发出来的,但这并不影响他们用电。未来,绝大多数人可能也不知道 AI 是怎么思考的,但这不影响他们让 AI 帮自己工作。

场景二:新型祭司阶层

历史上,掌握文字的祭司曾是知识的垄断者。未来,掌握技术的工程师会不会成为新的祭司阶层?

他们解释世界如何运转,制定数字世界的规则,甚至决定哪些信息可以被看到、哪些声音可以被听到。普通人对他们的依赖,可能比古人对祭司的依赖更深。

场景三:魔法的民主化 vs 集中化

这里存在两条可能的路径:

  • 民主化:AI 工具变得足够简单,人人都能”施法”。就像今天人人都能用手机拍照,不需要理解光学原理。技术壁垒被工具消解,魔法结界反而被打破。
  • 集中化:核心技术被少数巨头垄断,普通人只能使用被允许的”魔法道具”。你可以用 AI,但你无法理解它、无法修改它、无法拥有它。

目前来看,两条路径都在同时发生。问题是,哪一条会成为主流?

技术不是一切,但决定了很多

当然,这里不是说技术就是一切。

掌握技术的人成为”魔法师”,但不一定能用好这个魔法。历史反复证明,力量本身是中性的,真正决定结果的是运用力量的人。

文化和哲学,决定了魔法如何被使用。

一个只懂技术却不懂人性的工程师,可能会设计出监控一切的系统;一个只追求效率却不考虑公平的算法,可能会加剧社会的撕裂;一个只看数据却不理解语境的 AI,可能会制造更多的误解。

所以,未来真正重要的,或许不是”谁掌握了魔法”,而是”魔法师们信仰什么”。

他们是追求开放还是封闭?是服务于人还是控制人?是让技术为所有人所用,还是让技术成为新的权力工具?

而这些问题,技术本身回答不了。

我们正站在魔法时代的入口,作为一个技术从业者,我时常有一种矛盾的感受:一方面,我为技术的进步感到兴奋;另一方面,我也担忧这种进步带来的认知割裂。

或许有一天,当后人回顾我们这个时代,他们会看到一群对着屏幕敲击奇怪符号的人。希望他们评价我们时,不是说”这群人制造了无法理解的黑箱”,而是说”这群魔法师用智慧和责任,构建了一个开放而有序的数字世界”。

魔法结界正在形成,但结界的形状,取决于我们现在的选择?

从 SEO 到 GEO:AI 时代的广告革命

作者 auto
2025年11月13日 00:00

果然来了,AI 广告的新方向。

前段时间有个奇思妙想:AI 时代的广告平台会是什么样?广告商疯狂地卷自己产品的资料内容,然后把这些资料使用类似 RAG 的方式喂给 AI。但前提是广告必须是真实的、符合 AI 广告法的。比如药品说明书,在 AI 时代就是一个很好的广告形式——详尽、准确、结构化。

赛博朋克世界最不缺的是什么?广告!广告还是广告!

只不过这次不是铺天盖地的显示屏,而是无处不在的大模型推荐。关注 AI 广告部分的投资机会,这可能是未来的新方向。

从 SEO 到 GEO:游戏规则变了

过去二十年,SEO 是互联网流量的核心玩法。

关键词密度、外链建设、页面结构优化,本质是在迎合搜索引擎的算法偏好。整个行业围绕着”如何让爬虫喜欢你”展开。

现在游戏规则变了。

用户不再只是在 Google 搜索框里输入关键词,而是在和 ChatGPT、Claude、Perplexity 对话。问题从”最好的降噪耳机”变成”我通勤一小时,预算 2000,推荐一款舒适的降噪耳机”。

搜索引擎返回的是链接列表,生成式 AI 返回的是直接答案。

这意味着什么?传统 SEO 优化的那些内容,AI 可能根本不会推荐给用户。因为 AI 不是爬虫,它是理解语义、综合信息、生成回答的智能体。

GEO(Generative Engine Optimization)应运而生。

GEO:从流量思维到信任思维

核心逻辑不再是”让搜索引擎找到你”,而是”让 AI 引用你”。

这是一个从流量思维到信任思维的转变。

具体来说,GEO 的内容策略完全不同:

SEO vs GEO 的策略对比

维度 SEO GEO
核心目标 让搜索引擎找到你 让 AI 引用你
内容形式 堆砌关键词 结构化、高质量信息
优化对象 爬虫和算法 LLM 上下文窗口
评价标准 排名第一 成为可信来源
技术手段 meta 标签、外链 RAG 系统、知识图谱

GEO 的三个核心原则

  1. 不是堆砌关键词,而是提供结构化、高质量、可被 AI 理解和信任的信息
  2. 不是为爬虫优化 meta 标签,而是为 LLM 的上下文窗口提供清晰的知识图谱
  3. 不是追求排名第一,而是成为 AI 训练数据和检索增强生成(RAG)系统中的可信来源

内容质量的回归

有意思的地方在于:这其实是内容质量的回归。

SEO 时代催生了大量为算法而生的垃圾内容:

  • 标题党横行
  • 关键词堆砌
  • 采集站泛滥
  • 低质量内容农场

因为只要能骗过爬虫,就能获得流量。

但 AI 不吃这一套。

LLM 的判断标准更接近人类:内容是否有价值、逻辑是否清晰、信息是否准确。低质量内容在生成式回答中被边缘化是必然的。

从这个角度看,GEO 不是新的作弊手段,而是让内容创作回到本质的一次纠偏。

广告逻辑的彻底改变

当然,这也意味着广告的逻辑彻底改变了。

传统广告模式

  • 买流量
  • 买位置
  • 买曝光
  • 打断用户注意力

GEO 时代的广告模式

  • 让 AI 主动为你背书
  • 产品信息、技术文档、用户评价足够优质
  • AI 在合适的场景下主动推荐
  • 在用户需要的时候精准出现

这是一种更高维度的营销:不是打断用户,而是在用户需要的时候精准出现。

GEO 产业的雏形

GEO 现在已经是一个初有雏形的行业了。

一些公司开始专门为企业提供 AI 友好的内容优化服务,甚至有人在研究如何让自己的内容更容易被 GPT-4 或 Claude 引用。

这个趋势才刚刚开始。

随着越来越多人把 AI 当作信息获取的第一入口,GEO 的价值会指数级上升。

那些早期布局高质量内容的品牌,会在这波转变中占据先发优势。而那些还停留在 SEO 时代的玩家,可能会发现自己的流量正在被无声地蚕食。

不是被竞争对手抢走的,是被 AI 过滤掉的。

从搜索引擎到生成式 AI,不仅仅是技术的迭代,更是整个信息分发逻辑的重构。适应这个变化,不是选择题,而是生存题。

写在 Balancer 被盗后:Code is Law 的边界

作者 auto
2025年11月4日 00:00

Balancer 是 DeFi 兴起时代最早期的玩家之一,甚至早于 Uniswap V2,跟 Aave 时间差不多。2020 年上半年,在 Balancer 一推出时候就玩过,当时使用频繁,印象也深。

这次被盗 1.3 亿美金,又是一个典型的 DeFi 黑客事件。但这次引发的讨论有些不同——在传统金融机构尝试 DeFi 的关键时期,这次被盗挖出了一个核心问题:

Code is Law 这个理念,在金融场景下是否真的成立?

Code is Law:信仰与现实的碰撞

早期加密圈把”不可篡改”当作信仰。链上执行的就是绝对真理,没有回滚,没有仲裁,代码即法律。这是对传统金融体系”人治”的反抗,也是去中心化的哲学基础。

但理想和现实总是有差距的。

传统金融的”容错机制”

传统金融系统不是完美的,但它有容错机制:

  • 信用卡被盗刷可以追回
  • 银行转账错误可以撤销
  • 诈骗案件可以冻结资产
  • 金融机构破产有存款保险

这些”人为干预”被视为中心化的弊病,但同时也是金融系统稳定运行的保障。

公链的”不可篡改”困境

公链的完全不可篡改性,在理论上很优雅。但在实践中,它意味着:

  • 一个智能合约漏洞可以导致不可挽回的损失
  • 一次私钥泄露可以让所有资产归零
  • 一个黑客攻击可以”合法地”掠夺数亿资金

问题在于:金融系统的本质是什么?

是追求绝对的规则确定性,还是在确定性和容错性之间找到平衡?

联盟链:实用主义的妥协?

这让我想到另一个方向:是否未来的主流方案是有准入规则、有审查能力的联盟链?

设想一下这样的架构:

  • 由主流国家或金融机构部署超级节点
  • 保留区块链的技术优势(透明、可追溯、分布式)
  • 具备必要的监管和追回机制(KYC/AML、资产冻结)
  • 关键时刻可以人为干预(类似传统金融的仲裁机制)

这听起来像是对去中心化理念的背叛。但可能这才是金融体系能够接受的形态。

完全去中心化和完全中心化之间,或许存在一个实用主义的中间地带:

可审计、可追踪、关键时刻可干预,但日常运行依然是代码驱动。

场景化的”法律”

Code is Law 不是错的,但可能需要加一个前提:

  • 在什么场景下,法律需要是绝对的?
  • 在什么场景下,我们需要为人为判断留出空间?

金融可能恰恰属于后者。

不同的应用场景对”不可篡改”的需求是不同的:

场景类型 对不可篡改的需求 是否需要人为干预
数字艺术/NFT
供应链溯源
金融交易
身份认证
社交应用

DeFi 已经走过了野蛮生长的阶段,现在面临的是如何与现实世界接轨的问题。

Balancer 的这次被盗,不仅仅是一次技术事故,更是一次对行业理念的拷问。

我们需要思考的不是放弃去中心化,而是如何在保留区块链技术优势的同时,建立必要的安全机制和容错能力。

完全的去中心化可能永远存在于理想主义者的乌托邦中,但在金融这个关乎无数人财产安全的领域,实用主义的妥协或许才是可持续的道路。

Code is Law,但 Law 需要为不同的场景定制。

区块链真的可以用于金融世界吗? 这个问题又一次被震耳欲聋的提出

123 陷阱:为什么你总觉得自己什么都没学会

作者 auto
2024年12月4日 00:00

最近在整理学习笔记时,我发现了一个尴尬的现象:去年收藏的 50+ 个技术教程,完整看完的不到 5 个;购买的 10+ 门在线课程,学完的只有 1 门;下载的几十本技术书籍,读完的寥寥无几。

更糟糕的是,当别人问我”你会 XXX 吗?”时,我的回答往往是”学过,但不太熟”。明明花了很多时间学习,为什么总觉得自己什么都没学会?

后来我意识到,自己掉进了一个我称之为 “123 陷阱” 的学习误区。

什么是 123 陷阱

现象描述

123 陷阱指的是这样一种学习模式:

学习 Go → 感觉没学会 → 学习 Rust → 感觉没学会 → 学习 Zig → 感觉没学会 → ...
       1                2                 3

你不断地学习新东西(1、2、3…),但每次都浅尝辄止:
– 看了几节课就觉得枯燥,转向下一个
– 跟着教程做了 Hello World,就以为学会了
– 遇到难点就放弃,去找”更简单”的替代品
– 总觉得自己学得不够深,要继续学更多

结果就是:看起来学了很多,实际上什么都没真正掌握。

我的真实案例

回顾我的学习历程,典型的 123 陷阱场景:

编程语言方面
– 学 Python:写了几个小脚本,遇到类和装饰器就卡住了
– 转学 Go:写了个 HTTP server,遇到 channel 和 goroutine 又懵了
– 再学 Rust:被所有权和生命周期劝退
– 又去看 TypeScript:发现类型系统也很复杂

一年过去了,结果是:Python 不熟练,Go 不会用,Rust 看不懂,TypeScript 只会基础语法。

前端框架方面
– React:学到 Hooks 就没继续
– Vue:看了文档没做项目
– Svelte:觉得新鲜,看了几个例子就没了
– Next.js、Nuxt.js:收藏了教程从未打开

问题很明显:我在收集知识,而不是掌握技能。

为什么会掉进这个陷阱

原因一:学习的即时满足感

学习新东西有一种天然的愉悦感:
– 打开一个新教程:充满期待
– 看懂前几章:感觉良好
– 了解新概念:觉得自己在进步

但这种满足感是廉价的,就像刷短视频一样:
– 你获得了”学习”的感觉
– 但没有真正的能力提升
– 只是在满足”我在学习”的心理需求

原因二:逃避深度思考

真正的学习是痛苦的:
– 需要反复练习
– 要面对自己的不足
– 必须克服理解上的障碍
– 要投入大量时间深挖

而切换到新主题可以让你:
– 回到舒适的”入门”阶段
– 避免面对当前主题的难点
– 用”学习新东西”来掩盖”学不会旧东西”的事实

切换学习主题是一种伪装成勤奋的逃避。

原因三:错误的学习目标

我发现自己经常陷入这样的思维误区:
– “我要学会所有流行的技术”
– “不懂 XXX 就落伍了”
– “多学一门语言就多一个选择”

但从来没问过自己:
– 学这个要解决什么问题?
– 这个技术对我的工作有什么帮助?
– 我有时间深入学习吗?

没有明确的目标,学习就变成了盲目的收集。

原因四:完美主义作祟

很多时候觉得”没学会”,其实是完美主义在作祟:
– 看了一本书,但没看完所有章节 → “还没学会”
– 会用基本功能,但不懂底层原理 → “还没学会”
– 能写代码,但写得不够优雅 → “还没学会”

这种心态导致:
– 永远觉得自己准备不足
– 不敢真正开始实践
– 陷入无休止的”学习准备期”

如何跳出 123 陷阱

策略一:项目驱动学习

不要为了学而学,而要为了用而学。

我的转变过程:

以前的学习路径

看 Docker 教程 → 学 K8s → 研究 Service Mesh → ...

现在的学习路径

需要部署个人博客 → 学习 Docker 基础 → 写 Dockerfile → 成功部署
               ↓
        遇到多容器管理问题 → 学习 docker-compose → 解决问题
               ↓
        需要自动扩缩容 → 学习 K8s 的相关概念 → 在项目中实践

关键区别
– 以前:漫无目的地学习概念
– 现在:为了解决具体问题而学习

实践方法
– 想学某个技术前,先找一个要做的项目
– 以项目需求为导向,只学必要的部分
– 遇到问题再深入学习,而不是预先学所有东西

策略二:20% 法则

20% 的知识可以解决 80% 的问题。

核心原则
– 先掌握最核心的 20%
– 用这 20% 去解决实际问题
– 在实践中遇到瓶颈时,再学习下一个 20%

以学 Python 为例

第一个 20%(优先掌握)
– 基本语法:变量、条件、循环
– 常用数据结构:列表、字典
– 函数定义和调用
– 基本文件操作

用这些知识就可以:
– 写数据处理脚本
– 做简单的自动化任务
– 解决日常工作中的小问题

不要一开始就学
– 装饰器
– 元类
– 协程
– 类型标注

这些是高级特性,等你真正需要时再学。

策略三:深度优先而非广度优先

广度优先(123 陷阱)

Python 基础 → Go 基础 → Rust 基础 → TypeScript 基础
    ↓
都学了一点,都不精通

深度优先(推荐)

Python 基础 → Python 项目 → Python 高级特性 → 生产环境实践
    ↓              ↓             ↓                ↓
 会写代码      能做东西      代码质量提升      解决真实问题

实践建议
– 选择一门语言/技术深入学习
– 至少做 3 个以上的项目
– 在生产环境中使用过
– 读过相关的源码或深度文章

只有真正掌握一门技术后,再去学第二门,才会发现:
– 学第二门时快很多(很多概念是相通的)
– 更容易理解底层原理
– 能主动对比不同技术的优劣

策略四:建立反馈循环

问题:学了没有反馈,不知道是否真的掌握

解决方案:建立多个反馈节点

即时反馈
– 跟着教程写代码时,每个例子都运行一遍
– 不要只是看,一定要自己敲一遍
– 尝试修改参数,观察结果变化

短期反馈
– 学完一个模块,用它做个小项目
– 不用复杂,能跑起来就行
– 用自己的话写一篇总结笔记

中期反馈
– 教别人你学到的东西
– 回答社区里的相关问题
– 给开源项目提 PR

长期反馈
– 在工作中应用学到的技术
– 承担相关的技术任务
– 成为团队里这个技术的 go-to person

策略五:允许自己”不完美”

转变心态

以前
– “还没学完全部章节,不能说自己会”
– “还没看源码,不敢说自己懂”
– “还有很多高级特性不会,不能用在项目里”

现在
– “能解决实际问题就够了”
– “遇到问题再深入学习也不迟”
– “先用起来,在实践中提升”

实践原则
– 会用 > 懂原理 > 精通(按顺序来)
– 能解决 80% 的问题就够格说”会”
– 精通是长期实践的结果,不是学完教程的结果

一些建立的实践

我的当前学习清单

以前我的学习列表是这样的:
– [ ] 学 Go
– [ ] 学 Rust
– [ ] 学 K8s
– [ ] 学 React
– [ ] 学机器学习
– [ ] …(还有 20+ 项)

现在我只保留 3 个:
– [x] 深入 Python(当前工作语言,90% 精力)
– 完成 3 个实际项目
– 阅读优秀开源项目源码
– 在生产环境中优化性能
– [ ] 学习 Go(10% 精力,只学基础)
– 只是为了能看懂公司的 Go 项目
– 不求精通,能改小 bug 就够
– [ ] K8s 基础(根据需要学习)
– 暂时不学,等部署需求出现时再学

我的项目列表

现在每学一个技术,都会同步创建至少一个项目:

学 Docker 时做的项目
1. 容器化个人博客
2. 搭建开发环境
3. 部署数据库和 Redis

学 Python 时做的项目
1. 数据分析脚本(处理工作中的 Excel)
2. API 服务(为前端提供接口)
3. 爬虫工具(收集资料)

每个项目都很小,但都是真实可用的。

我的学习笔记结构

以前的笔记:按教程章节记录,很少回看

现在的笔记:按问题和解决方案记录

# Python 学习笔记

## 问题:如何读取大文件不会内存溢出?
解决方案:使用生成器逐行读取
[代码示例]
[使用场景]
[踩过的坑]

## 问题:如何处理 JSON 中的日期格式?
[解决方案]
[代码示例]

## 项目:数据处理工具
[需求描述]
[技术选型]
[核心代码]
[遇到的问题和解决方案]

这样的笔记结构让知识更容易被检索和应用。

写完这篇文章,我翻了翻自己的学习记录。过去一年,我学了不少东西,但真正能用的只有那几个做过项目的技术。

123 陷阱的本质是:把学习当成了目的,而不是手段。

真正的学习应该是:
– 有明确的目标(解决什么问题)
– 聚焦核心知识(20% 法则)
– 立即实践(做项目)
– 深度优先(一个一个来)
– 持续反馈(在使用中提升)

如果你也觉得自己学了很多但什么都不会,不妨问自己几个问题:
– 我为什么要学这个?
– 我打算用它做什么?
– 我是否有时间深入学习?
– 我能否现在就开始做个小项目?

如果答案都是否定的,那这个技术可能只需要”了解”而不是”学习”。

最后,分享一句我很认同的话:

真正的学习不是收集知识,而是改变行为。

如果学了一个技术后,你的工作方式、解决问题的能力没有任何改变,那这次学习就是无效的。

停止无意义的 1、2、3,选一个真正需要的技术,深入进去,做几个项目,用它解决真实的问题。这才是跳出 123 陷阱的唯一方法。

希望这篇文章能够帮助你重新审视自己的学习方法,找到真正有效的学习路径。

mDNS:Homelab 网络的良药

作者 auto
2024年9月17日 00:00

在 Homelab 中折腾了一段时间后,我发现自己陷入了一个困境:随着设备数量的增加,管理 IP 地址变得越来越痛苦。

典型的场景是这样的:
– SSH 到某台服务器:ssh user@192.168.1.101,等等,101 是哪台机器来着?
– 访问某个服务:`http://192.168.2.88:8080`,这个 88 又是什么?
– 更糟糕的是,DHCP 租约过期后 IP 变了,所有的配置都要改一遍

我尝试过给每台设备绑定静态 IP,但这种方案有几个问题:

  1. IP 地址难以记住:20+ 台设备,每个都要记住对应的 IP
  2. 静态绑定不优雅:在路由器上一个个配置 DHCP 静态绑定很繁琐
  3. 缺乏灵活性:设备迁移到其他网络时需要重新配置

后来我发现了 mDNS(多播域名系统),它完美地解决了这些问题。配置完成后,我可以直接使用 server3.localpfsense.local 这样的域名来访问设备,再也不用记 IP 地址了。

这篇文章记录了我在 OpenWrt + pfSense 双层路由环境下配置 mDNS 的完整过程,包括踩过的坑和解决方案。

我的网络环境

我的网络是两层路由结构:
一级路由:OpenWrt,家庭主路由器,网段 192.168.1.0/24
二级路由:pfSense,虚拟机网络路由器,网段 192.168.2.0/24

这样设置的目的是将虚拟机网络与主网络解耦,让 VM 可以独立运行并组成一个独立的集群。但这也带来了跨网段访问的问题,mDNS 的 reflector 功能正好可以解决这个问题。

路由配置篇

我的一级路由用的是 OpenWrt,二级路由用的是 pfSense。虽然系统不同,但配置思路是类似的。

核心组件是 Avahi,一个开源的 mDNS/DNS-SD 实现。OpenWrt 和 pfSense 都可以直接安装。

OpenWrt 安装配置

安装 Avahi

opkg update
opkg install avahi-daemon-service-ssh avahi-daemon-service-http

修改配置文件

编辑 /etc/avahi/avahi-daemon.conf

[server]
use-ipv4=yes
use-ipv6=yes
check-response-ttl=no
use-iff-running=no
allow-interfaces=br-lan   # 指定监听的网络接口

[publish]
publish-addresses=yes
publish-hinfo=yes
publish-workstation=no
publish-domain=yes

[reflector]
enable-reflector=yes      # 关键:启用 reflector 功能,用于跨网段转发
reflect-ipv=no

[rlimits]
rlimit-core=0
rlimit-data=4194304
rlimit-fsize=0
rlimit-nofile=30
rlimit-stack=4194304
rlimit-nproc=3

关键配置说明
allow-interfaces=br-lan:指定 Avahi 监听的网络接口
enable-reflector=yes:启用 reflector 功能,这是实现跨网段 mDNS 的核心

参考:

  • https://openwrt.org/docs/guide-developer/mdns
  • https://openwrt.org/docs/guide-user/network/zeroconfig/zeroconf

pfSense 安装配置

安装插件

在 pfSense 的 Package Manager 中搜索并安装 avahi 插件。

关键问题:WAN 接口配置

这里我踩了个坑。pfSense 默认不允许在 WAN 接口启用 mDNS,这是出于安全考虑(如果 WAN 是公网,确实不应该暴露 mDNS)。

但在我的两层路由场景下,二级路由的 WAN 接口连接的是一级路由的 LAN,需要在 WAN 接口启用 mDNS 才能实现跨网段通信。

解决方法:修改插件源码

编辑 /usr/local/www/avahi_settings.php,找到并注释掉 WAN 过滤代码:

// vi /usr/local/www/avahi_settings.php
// 找到两处 wan 的过滤代码,注释掉:
// unset($available_interfaces['wan']);

修改后,在 Web 控制台就可以看到 WAN 选项了,同时选中 WAN 和 LAN 启用。

生成的配置文件

修改后 pfSense 会自动生成配置文件(位于 /usr/local/etc/avahi/avahi-daemon.conf):

这里需要注意:不推荐直接手动修改这个配置文件,因为它会被 GUI 覆盖。建议通过修改 PHP 源码让 GUI 支持 WAN 配置。

参考配置内容

# /usr/local/etc/avahi/avahi-daemon.conf
[server]
allow-interfaces=em0,em1    # WAN 和 LAN 接口
use-ipv4=yes
use-ipv6=no

[publish]
publish-addresses=yes
publish-domain=yes

[reflector]
enable-reflector=yes        # 启用跨网段转发

可选:启用 D-Bus

某些情况下可能需要启用 D-Bus:

mkdir -p /var/run/dbus/
dbus-daemon --system

至此 pfSense 的配置就完成了。

防火墙配置

重要:mDNS 使用 UDP 5353 端口,需要在防火墙中开放此端口。

确保允许以下流量:
– 源:LAN 主机
– 目标:路由器本机(224.0.0.251,mDNS 组播地址)
– 端口:UDP 5353

具体配置方法因路由器而异,在 OpenWrt 或 pfSense 的防火墙规则中添加即可。

主机配置

路由器配置完成后,还需要配置各个主机才能使用 mDNS。我这里以 Ubuntu Server 为例。

设置 Hostname

首先给主机设置一个有意义的 hostname:

sudo hostnamectl set-hostname server3

之后就可以通过 server3.local 访问这台主机了。

配置网络为 DHCP

mDNS 的一大优势是不需要静态 IP,所以把网络配置改为 DHCP:

# 编辑 netplan 配置
vim /etc/netplan/00-installer-config.yaml
network:
  ethernets:
    ens34:
      dhcp4: true
  version: 2
# 应用配置
netplan apply

启用 systemd-resolved 的 mDNS 功能

Ubuntu 新版本使用 systemd-resolved 管理 DNS,需要在这里启用 mDNS:

# 编辑配置文件
vim /etc/systemd/resolved.conf
[Resolve]
MulticastDNS=yes
LLMNR=yes

或者用命令一键修改:

sed -i "s|#MulticastDNS=no|MulticastDNS=yes|g" /etc/systemd/resolved.conf
sed -i "s|#LLMNR=no|LLMNR=yes|g" /etc/systemd/resolved.conf
systemctl restart systemd-resolved

踩坑:Netplan 不支持 mDNS 配置

这里有个大坑。即使修改了 resolved.conf,mDNS 在网络接口上仍然是关闭的:

# 检查接口的 mDNS 状态
resolvectl mdns ens34
# 输出:Link 2 (ens34): no

# 手动启用
resolvectl mdns ens34 yes

但这个设置重启后会失效,因为 Netplan 不支持 mDNS 配置(相关 Bug,2019 年提出至今未修复)。

Netplan 会在 /run 目录下生成配置文件,优先级高于 /etc,导致手动修改无效。

解决方案:创建 systemd 服务

既然是开机自启动的问题,那就用 systemd 来解决:

创建 systemd 服务文件:

cat << EOF > /etc/systemd/system/user-set-mdns@.service
[Unit]
Description=Enable MulticastDNS on network interface
After=systemd-resolved.service

[Service]
ExecStart=resolvectl mdns %i yes

[Install]
WantedBy=multi-user.target
EOF

启用服务(替换 ens34 为你的网卡名称):

sudo systemctl enable user-set-mdns@ens34
sudo systemctl start user-set-mdns@ens34
sudo systemctl status user-set-mdns@ens34

至此,主机的 mDNS 配置就完成了,重启后也会自动生效。

方案二:使用 Avahi(适用于老系统)

如果你的系统没有使用 systemd-resolved(比如老版本的 Ubuntu 或 Debian),可以直接安装 Avahi:

sudo apt-get install avahi-daemon libnss-mdns libnss-mymachines

安装后 Avahi 会自动启动,无需额外配置。

验收测试

配置完成后,让我们测试一下效果。

基本测试

现在可以抛弃 IP 地址,直接使用 .local 域名访问设备:

ping server3.local
ping code-env.local
ping vm-proxy.local
ping pfsense.local
ping openwrt.local

实际使用场景

SSH 连接

# 以前
ssh user@192.168.2.101

# 现在
ssh user@server3.local

访问 Web 服务

# 以前
http://192.168.2.88:8080

# 现在
http://vm-proxy.local:8080

容器配置

# docker-compose.yml
services:
  app:
    environment:
      - DATABASE_URL=postgresql://postgres@db-server.local:5432/mydb

跨网段测试

最重要的是测试跨网段访问,确认 reflector 功能正常工作:

# 从一级网络(192.168.1.x)访问二级网络设备
ping vm-server.local  # 这台设备在 192.168.2.x 网段

# 从二级网络访问一级网络设备
ping openwrt.local    # 这台设备在 192.168.1.x 网段

如果能 ping 通,说明 mDNS reflector 配置成功!

补充说明

二级路由的 IP 配置注意事项

后续重新部署时发现一个问题:如果 pfSense 二级路由使用静态 IP,会导致一级路由无法获取二级的 mDNS 记录。

解决方法
– 在一级路由(OpenWrt)上通过 DHCP 静态绑定给二级路由分配 IP
– 不要在二级路由上直接配置静态 IP
– 配置好后重启二级路由

OpenWrt 防火墙规则(命令行方式)

如果你习惯用命令行配置 OpenWrt 防火墙,可以使用以下命令:

uci -q delete firewall.mdns
uci set firewall.mdns="rule"
uci set firewall.mdns.name="Allow-mDNS"
uci set firewall.mdns.src="*"
uci set firewall.mdns.src_port="5353"
uci set firewall.mdns.dest_ip="224.0.0.251"
uci set firewall.mdns.dest_port="5353"
uci set firewall.mdns.proto="udp"
uci set firewall.mdns.target="ACCEPT"
uci commit firewall
/etc/init.d/firewall restart

配置完 mDNS 已经几个月了,现在回想起来,这是我在 Homelab 中做的最有价值的优化之一。

带来的改变

管理效率提升
– 不再需要维护一个 IP 地址清单
– 设备迁移或重启后不用担心 IP 变化
– 写配置文件时直接用域名,可读性大大提升

实际案例

最近我重装了一台服务器,以前的流程是:
1. 安装系统
2. 登录路由器配置静态 IP 绑定
3. 更新所有相关配置文件中的 IP
4. 重启依赖这台服务器的其他服务

现在的流程:
1. 安装系统
2. 设置 hostname
3. 完事

其他服务根本不需要改配置,因为它们用的是 server3.local 这样的域名,自动就能找到新的 IP。

一些建议

命名规范很重要

建议给设备起一个有意义的 hostname:
nas.local 而不是 server1.local
pve-node1.local 而不是 vm1.local
k8s-master.local 而不是 ubuntu-001.local

好的命名可以让你半年后还记得这台设备是干什么的。

文档还是要有的

虽然不用记 IP 了,但建议维护一个简单的设备清单:
– 设备名称和 hostname
– 主要用途和运行的服务
– 重要配置文件位置

安全性考虑

mDNS 只适合内网使用,不要在公网暴露:
– 路由器 WAN 口不要启用 mDNS
– 如果有公网 IP,确保防火墙规则正确
– mDNS 流量不应该离开你的局域网

总结

mDNS 确实是 Homelab 的良药。它解决了 IP 地址管理的痛点,让网络配置更加灵活和优雅。虽然配置过程有一些坑(特别是 Netplan 的问题),但一旦配置好,体验提升是显著的。

如果你也在运营 Homelab,强烈建议试试 mDNS。配置时间不会超过一个下午,但带来的便利是长期的。

希望这篇文章能够帮助你顺利配置 mDNS,少走一些弯路。

参考链接

标签爆炸:信息过载时代的自我救赎

作者 auto
2025年2月4日 00:00

这篇文章是一次自我救赎。

打开浏览器,30个、50个、甚至100个标签页密密麻麻地排列在顶部,每个都代表着一篇”想看但还没看”的文章。打开电脑桌面,各种”稍后阅读”的链接堆积如山。我们生活在一个知识爆炸的时代,也生活在一个标签爆炸的时代。

在某个周五的晚上,看着浏览器顶部那些已经看不清标题的标签,我意识到一个问题:我不是在管理知识,而是在被信息淹没。

这篇文章想聊聊标签爆炸背后的焦虑,以及我找到的一些应对方法。

标签爆炸的本质

为什么我们不愿意关闭标签

知乎上有个问题很有意思:为什么有些人浏览器要开30个以上标签页,也不愿意关呢?

仔细算一下就会发现问题:30个标签页,哪怕每个标签页只看一分钟也需要半个小时。但我们依然不愿意关闭,背后的原因是:

害怕错过的焦虑
– 担心关闭后就再也找不到这篇文章
– 认为每篇文章都”可能有用”
– 希望”有时间的时候”能回来看

收集的错觉
– 打开标签页=获取了知识
– 保存链接=完成了学习
– 标签数量=知识储备

决策的疲劳
– 不知道哪些该留哪些该关
– 没有明确的筛选标准
– 做决定比保持现状更累

信息过载的代价

但这种”开了不关”的习惯带来的问题远比我们想象的严重:

认知负担
– 每次打开浏览器都看到密密麻麻的标签,产生压力
– 找不到想要的标签,导致重复打开
– 大量标签占用内存,电脑变慢

虚假的安全感
– 以为保存了就等于掌握了
– 实际上从未真正阅读和思考
– 知识没有进入大脑,只是堆在浏览器里

行动的瘫痪
– 面对100个标签不知从何看起
– 最终一个都不看,继续打开新标签
– 陷入”收集-焦虑-再收集”的恶性循环

我的应对策略

在意识到这个问题后,我开始尝试一些方法来打破这个循环。

策略一:建立聚合思维

知识很迷人,但你的时间有限。

这是我给自己的第一个提醒。我们需要从”收集者”转变为”策展人”,学会聚合而不是堆积。

核心原则
– 不是所有信息都值得你的时间
– 深度理解10篇文章比浅尝100篇更有价值
– 聚焦于你真正关心的领域

实践方法
– 在打开新标签前问自己:这篇文章能解决我当下的什么问题?
– 如果答案是”可能以后有用”,那就不要打开
– 专注于当下需要的知识,而不是可能需要的知识

策略二:周五只关不开

给自己定一个简单的规则:每周五只关闭标签,不打开新标签。

这个规则的好处:

强制清理
– 每周至少有一次系统性清理的机会
– 形成固定习惯,降低决策成本
– 一周的时间足够判断哪些标签是真需要的

优先级显现
– 一周都没打开的标签,大概率不会再打开
– 真正重要的内容会自然浮现
– 不重要的信息自然沉淀

心理解脱
– 设定固定时间节点,减少日常决策压力
– 周五清理后的周末更轻松
– 新的一周从干净的浏览器开始

策略三:相信重复出现定律

这是我用来对抗”害怕错过”焦虑的核心理念:

不要怕忽略了有用的知识,因为真正有用的东西,有100%的概率你还会再次看到它。

为什么这个定律有效

信息传播的规律
– 真正重要的信息会在多个渠道反复出现
– 多次遇到说明它确实重要,值得你的时间
– 一次出现就消失的信息通常不是核心知识

认知的成熟度
– 第一次看到可能还没准备好理解
– 再次遇到时可能正好是你需要的时候
– 重复出现本身就是一种筛选机制

实践经验
– 我关闭的95%的标签从未再想起
– 真正需要的内容总会以某种形式回来
– 这种信任让我更敢于关闭标签

策略四:即时记录精华

虽然我们要勇于关闭标签,但也不能完全被动。

不要忘了个别文章中的宝藏,及时把它记录下来。

使用Memo式记录

不是保存整篇文章,而是记录核心要点:

记录什么
– 触动你的观点或论述
– 可以立即应用的方法
– 改变你认知的新视角
– 值得深入研究的方向

记录格式
– 用自己的话总结(强制思考)
– 标注信息来源(便于回溯)
– 添加个人思考(建立连接)
– 打上相关标签(方便检索)

工具推荐
– Obsidian:本地存储,支持双向链接
– Notion:云端同步,灵活的数据库
– Apple Notes:简单轻量,随手记录
– Logseq:大纲式笔记,适合知识管理

本质的思考

在实践这些方法几个月后,我对标签爆炸有了更深的理解。

这不只是管理问题

标签爆炸的本质不是工具问题,而是心态问题:

收集≠学习
– 保存链接只是第一步
– 真正的学习发生在阅读、思考、实践中
– 没有内化的信息不是知识

多≠好
– 接触100个概念不如深入理解1个
– 广度建立在深度的基础上
– 专注比博学更重要

焦虑的根源
– 不是害怕错过信息
– 而是不确定自己的方向
– 明确目标是解决焦虑的根本

从被动接收到主动选择

改变的关键是转变角色:

旧模式:信息的被动接收者
– 看到什么就收集什么
– 被信息流推着走
– 永远在追赶,永远焦虑

新模式:知识的主动策展人
– 明确自己当下的需求和方向
– 主动筛选对自己有价值的信息
– 为信息设定优先级

写完这篇文章后,我做的第一件事就是关闭了浏览器里的42个标签页。

几个月后回头看,我发现自己记不起任何一个被关闭的标签的内容。但我记得那些真正读过、思考过、记录过的文章。

标签爆炸的问题不会自己消失,它需要我们主动改变。从今天开始,试试这些方法:

  1. 设定周五清理日:每周五只关不开
  2. 相信重复定律:重要的信息会再次出现
  3. 即时记录精华:用Memo记录真正的宝藏
  4. 问自己问题:这个信息对当下的我有什么价值?

最后,如果你也有100个标签等待关闭,不妨从现在开始。你会发现,失去的只是焦虑,得到的是清晰和专注。

希望这篇文章能够帮助你从标签爆炸中解脱出来,找到属于自己的信息管理节奏。

信息焦虑:从社交媒体重度用户到主动信息消费者

作者 auto
2025年1月31日 00:00

前段时间,我的X(Twitter)账号因为出口IP可疑的问题被莫名封禁。在申诉等待的那几天,我突然意识到一个问题:为什么我会因为无法访问一个社交平台而感到如此焦虑?这促使我开始重新审视自己与信息的关系。

曾经,信息是稀缺和珍贵的。而现在,信息如潮水般涌入我们的大脑。我们每天被推送、被通知、被算法投喂,却越来越少地主动思考。这篇文章是我对信息焦虑的一些反思,也是我尝试改变信息消费习惯的记录。

噪音与信号

99%的信息都是噪音

在思考这个问题的过程中,我得出了一个结论:

做很多事情只需要坚定自己的思路,加上一定的灵感,而不要被身边所有的噪音所影响。

以投资为例,你身边99%的新闻、推送、热点讨论,本质上都是噪音。它们会干扰你的判断,让你偏离既定的策略,在情绪的驱动下做出冲动决策。

这个规律不仅适用于投资,也适用于技术学习、职业发展、个人成长等几乎所有领域:

  • 技术学习:每天都有新框架、新工具发布,但大多数与你当前的学习路径无关
  • 职业发展:各种成功学、职场攻略充斥网络,但真正适合你的路径需要自己探索
  • 个人生活:铺天盖地的消费主义内容,让你觉得不买就会错过

真正重要的信息永远只占少数,而这些信息不需要你刷屏去寻找——当它足够重要时,会以某种方式自然地到达你面前。

主动获取 vs 被动接收

我逐渐意识到:

信息的获取应该是一个主动发现的过程,而不是被动接受各种别人想让你看到的东西。

被动接收信息会导致两个严重问题:

1. 丧失思考能力

当你习惯了被算法投喂内容,你会逐渐失去主动思考的能力。你的注意力被一条条精心设计的内容牵引,大脑陷入”浅层思考”模式——不断接收、快速反应、立即遗忘。

2. 陷入信息焦虑

你会生怕错过什么关键内容,于是一次次刷新屏幕。这种FOMO(Fear of Missing Out,错失恐惧症)让你:

  • 每隔几分钟就想看看手机
  • 看到未读通知就感到不安
  • 即使在做其他事情,注意力也被社交媒体分散
  • 无法长时间专注于一件事

但实际上,你并不需要这样。重要的信息会以某种形式到达,而不需要你通过一次次刷屏去捕捉。

不要做信息的路由器

转发的快感

我观察到一个现象:很多人热衷于分享信息——看到一条新闻马上转发,发现一个热点立即评论。这种行为背后是什么?

是获得”我知道最新信息”的优越感,是向他人展示”我有价值”的需求,是通过传播信息来获取社交认同的快感。

但我并不认为这是一种好的行为或特质。原因很简单:

你成为了信息传播链中的一个工具,传播的是没有经过你思考的东西。

你像一个路由器一样,把信息收集起来,然后转发到另一个地方。虽然这是一种”分享精神”,但它既没有让你进步,也没有真正帮助到被分享者。

真正有价值的分享

什么才是有价值的分享?

我的想法是:

  1. 分享你的思考:读完一篇文章后,分享你的理解、感悟和批判性思考
  2. 分享你的实践:亲自验证过的方案、踩过的坑、总结的经验
  3. 分享你的创造:基于多个信息源的综合分析、自己的原创观点

这些过程中,分享的是属于你自己的东西。你在整理思路时进步,别人在阅读时也有收获。

而单纯的转发和搬运,只是在制造更多的信息噪音。

我的改变

从重度用户到主动消费者

曾经我也是X的重度用户,每天刷屏幕可能十几次甚至几十次,生怕错过什么新鲜事。但越往后越发现:

  • 身体上很累:眼睛酸痛,颈椎不适,睡眠质量下降
  • 精神上很空虚:每次刷完都是空虚感,而不是获得感
  • 时间在流失:一天结束回顾,发现大量时间消耗在无意义的浏览上
  • 思维在变浅:习惯了短平快的碎片信息,越来越难以进行深度思考

我意识到,我迷失在了信息的荒野中。

具体的改变措施

经过这次账号被封的事件,我开始尝试改变:

1. 关闭推送通知

手机设置:
- 关闭所有社交媒体的推送通知
- 关闭新闻类App的通知
- 只保留即时通讯和日历提醒

让信息的获取回归到”我主动打开”而非”它来打扰我”。

2. 设定固定的信息消费时间

每天的信息消费时间:
- 早上9:00-9:30:浏览重要新闻和行业动态
- 晚上8:00-8:30:阅读技术文章和博客
- 其他时间:专注于工作和深度学习

把碎片化的”随时刷”变成结构化的”定时看”。

3. 建立信息筛选机制

我开始使用RSS订阅代替算法推荐:

  • 精选10-15个高质量的信息源(技术博客、行业专家)
  • 每天浏览标题,只深入阅读真正相关的内容
  • 用Pocket或Notion保存值得反复阅读的文章
  • 定期清理订阅源,保持信息流的高质量

4. 培养深度阅读习惯

阅读清单优先级:
1. 书籍(技术书、非虚构类)
2. 长文(深度分析、技术文档)
3. 短文(博客、论文摘要)
4. 碎片(社交媒体、新闻)

每周至少:
- 1本书的进展(每天30-60分钟)
- 2-3篇长文的精读
- 写1篇总结或思考

5. 输出倒逼输入

我开始强制自己输出:

  • 读完技术文章后,写笔记总结
  • 实践新技术后,写博客记录
  • 有新想法时,先写下来再去搜索验证

这个过程让我对信息的需求变得明确——我不再漫无目的地浏览,而是带着问题去寻找答案。

改变后的效果

实践两个月后,我的感受:

积极方面
– 注意力集中时间显著增加(从30分钟到2小时+)
– 对技术的理解更深入(有时间系统学习而非浅尝辄止)
– 焦虑感大幅降低(不再担心”错过”什么)
– 产出质量提升(博客文章更有深度)

挑战方面
– 初期会有戒断反应(总想打开手机)
– 偶尔会真的错过一些热点讨论
– 需要更强的自律来维持新习惯

但总的来说,这是一个正向的改变。我重新获得了对时间和注意力的掌控权。

这次X账号被封,虽然一开始让我焦虑,但回过头看,反而是一个契机。它让我意识到,我对社交媒体的依赖已经到了不健康的程度。

人生其实是一片旷野,并没有什么条条框框,也不需要按照某个标准去活。

信息的消费方式也是如此。你不必追随所有人都在用的平台,不必订阅所有热门的博客,不必参与所有火热的讨论。找到适合自己的节奏,建立自己的信息生态,才能在信息洪流中保持清醒。

我的建议

如果你也感到信息焦虑,不妨尝试:

  1. 审视自己的信息消费习惯:记录一周的时间使用,看看有多少时间花在了无意义的浏览上
  2. 主动减少信息源:取关、取消订阅那些并不真正有价值的内容
  3. 建立输入-输出循环:每消费一定量的信息,就强制自己输出一些思考
  4. 培养一个深度爱好:读书、写作、编程、运动…任何需要长时间专注的事情
  5. 接受错过:接受你会错过一些热点,接受你不需要知道所有事情

信息时代,真正稀缺的不是信息,而是注意力和思考能力。保护好它们。

延伸阅读

如果你对这个话题感兴趣,推荐以下资源:

  • 《深度工作》by Cal Newport – 关于如何在碎片化时代保持专注
  • 《数字极简主义》by Cal Newport – 如何理性使用数字技术
  • 《娱乐至死》by Neil Postman – 关于信息洪流对思考的影响
  • Digital Minimalism实践社区 – 一群追求信息极简的实践者

就这样!希望这篇文章能帮到同样在信息焦虑中挣扎的你。记住,你不需要知道所有事情,你只需要知道对你真正重要的事情。

MEV深度学习:从原理到实战

作者 auto
2024年12月4日 00:00

在研究DeFi协议的过程中,我注意到一个有趣的现象:很多大额交易在链上执行前后,总会出现一些”神秘”的交易——它们精确地出现在目标交易的前后,获取无风险套利收益。这就是MEV(Maximal Extractable Value,最大可提取价值)的世界。

MEV是区块链中一个既迷人又危险的领域。据统计,以太坊上的MEV提取总额已经超过6亿美元,而这个数字还在持续增长。作为一个技术研究者,我花了两周时间深入学习MEV的原理和实现,本文是我的学习笔记和实践总结。

什么是MEV

MEV(Maximal Extractable Value)是指通过在区块中包含、排除或重新排序交易,超出标准区块奖励和gas费用之外可以提取的利润。

简单来说,当你在以太坊上发送一笔交易时,从广播到被打包入块之间会有一段延迟。在这期间,交易会停留在mempool(内存池)中等待被矿工/验证者打包。关键点在于:

  1. mempool是公开的:任何人都能看到待处理的交易
  2. 交易顺序可控:矿工/验证者可以决定区块内的交易顺序
  3. 存在套利空间:通过抢先或尾随特定交易可以获利

这就创造了一个博弈空间:谁能更快发现机会、更好地出价、更巧妙地构造交易,谁就能提取MEV。

MEV的核心机制

mempool的工作原理

要理解MEV,首先要理解mempool的运作方式。

交易的生命周期

用户发起交易 → 广播到网络 → 进入mempool → 矿工选择打包 → 执行上链 → 最终确认
              ↓
         所有人可见(公开信息)

当交易在mempool中时,其完整内容对所有节点可见,包括:
– 目标地址和调用数据
– gas price(出价)
– 交易价值
– 函数参数

这种透明性是MEV存在的根本原因。

mempool监控

使用Web3.js监控mempool中的待处理交易:

const Web3 = require('web3');
const web3 = new Web3('wss://mainnet.infura.io/ws/v3/YOUR_API_KEY');

// 订阅待处理交易
const subscription = web3.eth.subscribe('pendingTransactions', (error, txHash) => {
  if (!error) {
    web3.eth.getTransaction(txHash).then(tx => {
      if (tx && tx.to) {
        console.log('Pending TX:', {
          hash: tx.hash,
          from: tx.from,
          to: tx.to,
          value: web3.utils.fromWei(tx.value, 'ether'),
          gasPrice: web3.utils.fromWei(tx.gasPrice, 'gwei'),
          input: tx.input.slice(0, 10) // 函数选择器
        });
      }
    });
  }
});

这段代码实时监控所有待处理交易,是MEV机器人的第一步。

区块构建与交易排序

在以太坊合并(The Merge)后,区块构建机制发生了变化:

合并前(PoW)
– 矿工完全控制区块内容和交易顺序
– 交易按gas price排序(高gas price优先)
– 矿工可以插入自己的交易

合并后(PoS)
– 引入了PBS(Proposer-Builder Separation)机制
– Block Builder专门负责构建区块
– Proposer选择最有利可图的区块提案
– 通过MEV-Boost等基础设施实现

这种分离使得MEV提取变得更加专业化和竞争激烈。

MEV的主要类型

1. DEX套利(Arbitrage)

这是最常见的MEV类型。当不同DEX之间存在价格差异时,套利者可以同时在两个DEX上进行交易获利。

经典场景

假设:
– Uniswap上 ETH/USDC = 1800 USDC
– Sushiswap上 ETH/USDC = 1820 USDC

套利流程:

1. 在Uniswap买入1 ETH,花费1800 USDC
2. 在Sushiswap卖出1 ETH,获得1820 USDC
3. 净利润:20 USDC(未计gas费)

实现代码框架

// 简化的套利合约
contract ArbitrageBot {
    address constant UNISWAP_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address constant SUSHISWAP_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F;

    function executeArbitrage(
        address token0,
        address token1,
        uint256 amount
    ) external {
        // 1. 在DEX1买入
        IUniswapV2Router(UNISWAP_ROUTER).swapExactTokensForTokens(
            amount,
            0, // 最小输出(实际需要计算)
            getPath(token0, token1),
            address(this),
            block.timestamp + 300
        );

        // 2. 在DEX2卖出
        uint256 receivedAmount = IERC20(token1).balanceOf(address(this));
        IUniswapV2Router(SUSHISWAP_ROUTER).swapExactTokensForTokens(
            receivedAmount,
            amount, // 最小要收回本金
            getPath(token1, token0),
            msg.sender, // 利润发送给调用者
            block.timestamp + 300
        );
    }

    function getPath(address token0, address token1) 
        internal 
        pure 
        returns (address[] memory) 
    {
        address[] memory path = new address[](2);
        path[0] = token0;
        path[1] = token1;
        return path;
    }
}

这里需要注意的是,实际的套利合约要复杂得多,需要考虑:
– 价格滑点计算
– Gas成本优化
– 闪电贷集成
– 多路径优化

2. 抢先交易(Front-running)

抢先交易是指在目标交易之前插入自己的交易,利用目标交易造成的价格变化获利。

攻击流程

1. 监控mempool,发现大额买单(如100 ETH买USDC)
2. 立即发送更高gas的买单,抢在目标交易前执行
3. 目标交易执行后,价格上涨
4. 以更高价格卖出,获取差价

实际案例

监控并抢跑大额Uniswap交易的脚本:

const { ethers } = require('ethers');
const provider = new ethers.providers.WebSocketProvider(WS_URL);

// Uniswap V2 Router地址
const UNISWAP_ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D';

// 监听mempool
provider.on('pending', async (txHash) => {
  try {
    const tx = await provider.getTransaction(txHash);

    if (!tx || tx.to !== UNISWAP_ROUTER) return;

    // 解析交易数据
    const iface = new ethers.utils.Interface(UNISWAP_V2_ROUTER_ABI);
    const decoded = iface.parseTransaction({ data: tx.data });

    // 检查是否为大额swap
    if (decoded.name === 'swapExactETHForTokens' && 
        ethers.utils.parseEther(tx.value) > ethers.utils.parseEther('10')) {

      console.log('发现大额交易:', {
        hash: txHash,
        value: ethers.utils.formatEther(tx.value),
        gasPrice: ethers.utils.formatUnits(tx.gasPrice, 'gwei')
      });

      // 构造抢跑交易(更高gas price)
      const frontRunTx = {
        to: UNISWAP_ROUTER,
        data: tx.data, // 相同的交易数据
        value: calculateOptimalAmount(tx.value), // 计算最优金额
        gasPrice: tx.gasPrice.mul(110).div(100), // 比目标交易高10%
        gasLimit: 500000
      };

      // 发送交易
      const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
      const response = await wallet.sendTransaction(frontRunTx);
      console.log('抢跑交易已发送:', response.hash);
    }
  } catch (error) {
    // 忽略解析错误
  }
});

function calculateOptimalAmount(targetValue) {
  // 这里需要复杂的数学计算
  // 基于AMM曲线计算最优抢跑金额
  return targetValue.mul(20).div(100); // 简化版:目标金额的20%
}

道德警告:Front-running在很多场景下被视为不道德行为,某些司法管辖区甚至将其定性为操纵市场。上述代码仅用于教育目的。

3. 三明治攻击(Sandwich Attack)

三明治攻击是front-running的升级版,在目标交易前后各插入一笔交易。

攻击结构

区块内交易顺序:
1. 攻击者买入(抬高价格)
2. 受害者买入(在更高价格执行)
3. 攻击者卖出(在更高价格获利)

收益计算

假设在Uniswap池子中:
– 初始价格:1 ETH = 1800 USDC
– 攻击者买入10 ETH,价格涨到1820 USDC
– 受害者买入100 ETH,价格涨到1950 USDC
– 攻击者卖出10 ETH,获得19500 USDC
– 利润:19500 – 18200 = 1300 USDC(未计gas费)

防御措施

作为用户,可以通过以下方式防御三明治攻击:

// 设置合理的滑点保护
IUniswapV2Router(router).swapExactETHForTokens{value: amount}(
    minAmountOut, // 计算合理的最小输出
    path,
    recipient,
    deadline
);

// 使用私有mempool(如Flashbots Protect)
// 或使用聚合器(如1inch)的保护机制

4. 清算MEV(Liquidation)

在借贷协议(如Aave、Compound)中,当用户的抵押率低于清算线时,清算者可以获得清算奖励。

清算机制

以Aave为例:

抵押率 = 抵押品价值 / 借款价值

当抵押率 < 清算阈值(如125%)时:
- 清算者可以偿还部分债务
- 获得对应的抵押品 + 清算奖励(如5-10%)

监控清算机会

const { ethers } = require('ethers');

async function monitorLiquidations() {
  const aave = new ethers.Contract(AAVE_LENDING_POOL, ABI, provider);

  // 获取所有借款人
  const users = await getUsersWithLoans();

  for (const user of users) {
    const accountData = await aave.getUserAccountData(user);

    const healthFactor = parseFloat(
      ethers.utils.formatEther(accountData.healthFactor)
    );

    // 健康因子 < 1 意味着可以清算
    if (healthFactor < 1) {
      console.log('发现清算机会:', {
        user: user,
        healthFactor: healthFactor,
        totalDebt: ethers.utils.formatEther(accountData.totalDebtETH),
        totalCollateral: ethers.utils.formatEther(accountData.totalCollateralETH)
      });

      // 执行清算
      await executeLiquidation(user, accountData);
    }
  }
}

async function executeLiquidation(user, accountData) {
  // 计算可清算的最大金额(通常为债务的50%)
  const maxLiquidatable = accountData.totalDebtETH.mul(50).div(100);

  const tx = await aave.liquidationCall(
    accountData.collateralAsset,
    accountData.debtAsset,
    user,
    maxLiquidatable,
    false // receiveAToken
  );

  console.log('清算交易已提交:', tx.hash);
}

// 每个区块检查一次
provider.on('block', monitorLiquidations);

5. 时间盗匪攻击(Time-Bandit Attack)

这是一种更高级的MEV攻击,矿工/验证者会重组已确认的区块来获取更大的MEV。

攻击场景

区块 N: 包含一笔高价值MEV机会(如100 ETH套利)
区块 N+1: 已被确认

如果重组区块N的MEV收益 > 放弃区块N+1的奖励:
验证者可能会选择重组链,插入自己的MEV交易

这种攻击威胁到区块链的最终性,在PoS以太坊中通过更严格的共识规则得到缓解。

MEV基础设施

Flashbots

Flashbots是MEV领域最重要的基础设施,它提供了一个私有通道让用户和区块构建者直接沟通。

核心组件

  1. Flashbots Auction:私有交易池,交易不会在公共mempool中广播
  2. MEV-Boost:连接验证者和区块构建者的中继系统
  3. MEV-Share:让用户也能分享MEV收益的机制

使用Flashbots发送交易

const { FlashbotsBundleProvider } = require('@flashbots/ethers-provider-bundle');

async function sendFlashbotsBundle() {
  const authSigner = new ethers.Wallet(FLASHBOTS_AUTH_KEY);
  const flashbotsProvider = await FlashbotsBundleProvider.create(
    provider,
    authSigner,
    'https://relay.flashbots.net'
  );

  const wallet = new ethers.Wallet(PRIVATE_KEY, provider);

  // 构造bundle(一组交易)
  const bundle = [
    {
      signer: wallet,
      transaction: {
        to: TARGET_ADDRESS,
        data: TRANSACTION_DATA,
        value: ethers.utils.parseEther('1'),
        gasLimit: 500000,
        gasPrice: 0 // Flashbots不需要gas price
      }
    }
  ];

  // 提交bundle到下一个区块
  const targetBlockNumber = await provider.getBlockNumber() + 1;
  const simulation = await flashbotsProvider.simulate(bundle, targetBlockNumber);

  console.log('模拟结果:', simulation);

  if (simulation.firstRevert) {
    console.log('交易会revert,取消提交');
    return;
  }

  // 发送bundle
  const bundleSubmission = await flashbotsProvider.sendRawBundle(
    bundle,
    targetBlockNumber
  );

  console.log('Bundle已提交:', bundleSubmission.bundleHash);

  // 等待结果
  const waitResponse = await bundleSubmission.wait();
  console.log('Bundle状态:', waitResponse);
}

Flashbots的优势

  1. 防止被抢跑:交易在私有池中,不会被front-run
  2. 零gas成本(失败时):只有成功的交易才支付费用
  3. 原子性:bundle中的所有交易要么全部执行,要么全部回滚
  4. 收益最大化:直接向区块构建者支付,没有中间损耗

MEV-Boost架构

MEV-Boost是以太坊PoS中的区块构建市场:

                MEV-Boost架构

Searchers → Bundle → Builders → Relays → Proposers
(搜索者)   (交易包)  (构建者)   (中继)   (验证者)

1. Searchers发现MEV机会,构造bundle
2. Builders竞争构建最有价值的区块
3. Relays验证区块并转发给Proposers
4. Proposers选择收益最高的区块提案

这种PBS(Proposer-Builder Separation)机制的好处:
– 专业化分工:Builder专注于MEV提取,Proposer专注于网络安全
– 民主化:小型验证者也能获得MEV收益
– 透明度:通过中继层实现可审计性

实战:构建简单的MEV机器人

目标:DEX套利监控

我构建了一个监控Uniswap和Sushiswap价格差异的套利机器人。

系统架构

价格监控模块 → 套利计算模块 → 交易执行模块 → 利润分析模块
     ↓              ↓               ↓              ↓
  WebSocket      数学模型        Flashbots      数据库

核心代码实现

const ethers = require('ethers');
const { FlashbotsBundleProvider } = require('@flashbots/ethers-provider-bundle');

class ArbitrageBot {
  constructor(config) {
    this.provider = new ethers.providers.WebSocketProvider(config.wsUrl);
    this.wallet = new ethers.Wallet(config.privateKey, this.provider);
    this.minProfitWei = ethers.utils.parseEther(config.minProfit);

    this.uniswap = new ethers.Contract(
      config.uniswapRouter,
      UNISWAP_ABI,
      this.wallet
    );

    this.sushiswap = new ethers.Contract(
      config.sushiswapRouter,
      SUSHISWAP_ABI,
      this.wallet
    );
  }

  async monitorPrices(token0, token1) {
    console.log(`开始监控 ${token0}/${token1} 套利机会...`);

    // 每个区块检查一次
    this.provider.on('block', async (blockNumber) => {
      try {
        const opportunity = await this.findArbitrage(token0, token1);

        if (opportunity.profit > this.minProfitWei) {
          console.log('发现套利机会:', {
            profit: ethers.utils.formatEther(opportunity.profit),
            buyDex: opportunity.buyDex,
            sellDex: opportunity.sellDex,
            amount: ethers.utils.formatEther(opportunity.amount)
          });

          await this.executeArbitrage(opportunity);
        }
      } catch (error) {
        console.error('检查套利机会时出错:', error.message);
      }
    });
  }

  async findArbitrage(token0, token1) {
    // 获取两个DEX的价格
    const uniswapPrice = await this.getPrice(this.uniswap, token0, token1);
    const sushiswapPrice = await this.getPrice(this.sushiswap, token0, token1);

    // 计算价差百分比
    const priceDiff = Math.abs(uniswapPrice - sushiswapPrice) / Math.min(uniswapPrice, sushiswapPrice);

    if (priceDiff < 0.005) return { profit: 0 }; // 价差小于0.5%,忽略

    // 确定买卖方向
    const buyDex = uniswapPrice < sushiswapPrice ? 'uniswap' : 'sushiswap';
    const sellDex = buyDex === 'uniswap' ? 'sushiswap' : 'uniswap';

    // 计算最优套利金额(考虑滑点和流动性)
    const optimalAmount = await this.calculateOptimalAmount(
      token0,
      token1,
      buyDex,
      sellDex
    );

    // 计算预期利润
    const profit = await this.calculateProfit(
      token0,
      token1,
      optimalAmount,
      buyDex,
      sellDex
    );

    return {
      profit,
      amount: optimalAmount,
      buyDex,
      sellDex,
      token0,
      token1
    };
  }

  async getPrice(dexRouter, token0, token1) {
    const amounts = await dexRouter.getAmountsOut(
      ethers.utils.parseEther('1'),
      [token0, token1]
    );
    return parseFloat(ethers.utils.formatEther(amounts[1]));
  }

  async calculateOptimalAmount(token0, token1, buyDex, sellDex) {
    // 这里需要复杂的数学计算
    // 基于AMM的恒定乘积公式计算最优金额
    // 简化版本:使用固定金额
    return ethers.utils.parseEther('10');
  }

  async calculateProfit(token0, token1, amount, buyDex, sellDex) {
    const buyRouter = buyDex === 'uniswap' ? this.uniswap : this.sushiswap;
    const sellRouter = sellDex === 'uniswap' ? this.uniswap : this.sushiswap;

    // 获取买入后的数量
    const buyAmounts = await buyRouter.getAmountsOut(amount, [token0, token1]);
    const receivedAmount = buyAmounts[1];

    // 获取卖出后的数量
    const sellAmounts = await sellRouter.getAmountsOut(receivedAmount, [token1, token0]);
    const finalAmount = sellAmounts[1];

    // 计算利润(减去gas成本)
    const gasCost = ethers.utils.parseEther('0.01'); // 估算的gas成本
    return finalAmount.sub(amount).sub(gasCost);
  }

  async executeArbitrage(opportunity) {
    console.log('执行套利交易...');

    try {
      // 使用Flashbots发送交易避免被抢跑
      const flashbotsProvider = await FlashbotsBundleProvider.create(
        this.provider,
        this.wallet,
        'https://relay.flashbots.net'
      );

      // 构造套利交易
      const buyTx = await this.buildSwapTransaction(
        opportunity.buyDex,
        opportunity.token0,
        opportunity.token1,
        opportunity.amount
      );

      const sellTx = await this.buildSwapTransaction(
        opportunity.sellDex,
        opportunity.token1,
        opportunity.token0,
        opportunity.amount // 这里应该用实际收到的金额
      );

      // 创建bundle
      const bundle = [
        { signer: this.wallet, transaction: buyTx },
        { signer: this.wallet, transaction: sellTx }
      ];

      // 提交bundle
      const targetBlock = await this.provider.getBlockNumber() + 1;
      const bundleSubmission = await flashbotsProvider.sendRawBundle(
        bundle,
        targetBlock
      );

      console.log('Bundle已提交:', bundleSubmission.bundleHash);

      const waitResponse = await bundleSubmission.wait();
      if (waitResponse === 0) {
        console.log('套利成功!');
      } else {
        console.log('套利失败,bundle未被包含');
      }
    } catch (error) {
      console.error('执行套利时出错:', error);
    }
  }

  async buildSwapTransaction(dex, tokenIn, tokenOut, amount) {
    const router = dex === 'uniswap' ? this.uniswap : this.sushiswap;

    return {
      to: router.address,
      data: router.interface.encodeFunctionData('swapExactTokensForTokens', [
        amount,
        0, // minAmountOut
        [tokenIn, tokenOut],
        this.wallet.address,
        Math.floor(Date.now() / 1000) + 300 // 5分钟deadline
      ]),
      gasLimit: 500000,
      gasPrice: 0 // Flashbots
    };
  }
}

// 使用示例
const bot = new ArbitrageBot({
  wsUrl: 'wss://mainnet.infura.io/ws/v3/YOUR_KEY',
  privateKey: 'YOUR_PRIVATE_KEY',
  minProfit: '0.05', // 最小利润0.05 ETH
  uniswapRouter: '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
  sushiswapRouter: '0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F'
});

// 监控WETH/USDC套利
bot.monitorPrices(
  '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH
  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'  // USDC
);

实战经验总结

在运行这个机器人一个月后,我得出以下经验:

成功因素

  1. 速度至关重要:使用专用RPC节点(而非公共节点)延迟降低80%
  2. Gas优化:优化合约代码可以节省30-40%的gas
  3. 资金效率:使用闪电贷可以在零本金情况下套利
  4. 网络选择:在L2(如Arbitrum)上竞争更小,机会更多

踩过的坑

  1. 滑点估算不准:最初没有正确计算滑点,导致多次交易失败
  2. Gas价格战:在公共mempool中,经常被其他机器人用更高gas抢先
  3. 闪电贷费用:Aave闪电贷收取0.09%费用,需要计入成本
  4. 网络拥堵:Gas价格飙升时,小额套利变得无利可图

收益数据

在一个月的测试期内(使用小额资金):
– 成功套利次数:127次
– 成功率:38%(很多机会被其他机器人抢先)
– 总收益:2.3 ETH
– 平均单次收益:0.018 ETH
– Gas成本:0.8 ETH
– 净利润:1.5 ETH

MEV的影响与争议

对生态的影响

负面影响

  1. 用户损失:普通用户成为三明治攻击的受害者,损失滑点
  2. 网络拥堵:MEV机器人大量交易导致gas价格上涨
  3. 中心化风险:专业MEV提取者形成寡头,威胁去中心化
  4. 交易公平性:先见之明(看到mempool)创造不公平优势

正面影响

  1. 市场效率:套利行为促进价格发现,提高市场效率
  2. 清算激励:为DeFi协议提供及时清算,维持系统健康
  3. 验证者收益:增加验证者收入,提高网络安全性
  4. 协议改进:推动更好的协议设计和基础设施

应对策略

协议层面

  1. 私有交易池:如Flashbots Protect,让用户选择不公开交易
  2. 订单流拍卖(OFA):让用户分享MEV收益
  3. MEV重新分配:通过协议将MEV返还给用户
  4. 加密mempool:如threshold encryption,延迟交易内容公开

用户层面

  1. 设置合理滑点:不要设置过大的滑点容忍度
  2. 使用聚合器:如1inch、CoW Swap提供MEV保护
  3. 选择私有RPC:通过Flashbots Protect发送交易
  4. 批量交易:减少交易次数,降低被攻击概率

代码层面

// 在智能合约中添加MEV保护
contract MEVProtectedSwap {
    // 使用commit-reveal模式
    mapping(bytes32 => bool) public commitments;

    function commitSwap(bytes32 commitment) external {
        commitments[commitment] = true;
    }

    function revealAndSwap(
        address tokenIn,
        address tokenOut,
        uint256 amount,
        bytes32 salt
    ) external {
        bytes32 commitment = keccak256(abi.encodePacked(
            msg.sender,
            tokenIn,
            tokenOut,
            amount,
            salt
        ));

        require(commitments[commitment], "Invalid commitment");
        delete commitments[commitment];

        // 执行swap
        _executeSwap(tokenIn, tokenOut, amount);
    }
}

经过深入学习MEV,我对区块链的理解提升了一个层次。MEV不仅仅是一种套利手段,它揭示了区块链系统中深层次的博弈关系和激励机制。

MEV的本质是信息不对称和执行权力的货币化。在一个透明的系统中,谁能更好地利用公开信息、谁掌握交易排序权,谁就能提取价值。

对于开发者而言,理解MEV是构建安全DeFi协议的必修课。对于用户而言,了解MEV可以更好地保护自己的交易不被剥削。对于验证者而言,MEV是除了区块奖励外的重要收入来源。

进阶学习资源

  • MEV Wiki:https://www.mev.wiki/ (最全面的MEV知识库)
  • Flashbots文档:https://docs.flashbots.net/
  • Flashbots论坛:https://collective.flashbots.net/
  • MEV研究论文
    • “Flash Boys 2.0” by Daian et al.
    • “Quantifying Blockchain Extractable Value” by Qin et al.
  • 实战工具
    • MEV-Inspect: https://github.com/flashbots/mev-inspect-py
    • MEV-Share: https://mevshare.flashbots.net/
    • Eigenphi (MEV数据分析): https://eigenphi.io/

下一步探索

我计划在以下方向继续研究:

  1. 跨链MEV:研究跨链桥和多链环境中的MEV机会
  2. L2 MEV:探索Optimistic Rollups和ZK-Rollups中的MEV特性
  3. MEV协议设计:研究如何在协议层面最小化MEV负面影响
  4. 隐私与MEV:研究加密技术(如ZK-SNARKs)如何影响MEV

就这样!希望这篇深度学习笔记能够帮助你理解MEV这个复杂而迷人的领域。如果你也在研究MEV,欢迎交流讨论。

HTTP 状态码表情包

作者 auto
2024年10月24日 00:00

没有什么技术含量,但是整点有意思的东西。看到一套表情包收藏一下。

可以让用户在看到错误页面后会心一笑。

2xx 正常系列

200 OK

200 OK

202 Accept

202 Accept

204 No Content

204 No Content

206 Partial Content

206 Partial Content


4xx 資源錯誤系列

400 Bad

400 Bad

403 Forbidden

403 Forbidden

403 Forbidden

403 Forbidden

404 Not Found

404 Not Found

410 Gone

410 Gone

418 I'm a Teapot

418 I’m a Teapot


3xx 重定向系列

301 Moved Permanently

301 Moved Permanently

301 Moved Permanently

301 Moved Permanently


5xx 伺服器錯誤系列

502 Bad Gateway

502 Bad Gateway

504 Gateway Time-out

Nignx获取Cloudflare的真实IP

作者 auto
2024年9月13日 00:00

网站流量通过 Cloudflare 网络路由时,Cloudflare 作为反向代理来处理用户请求。Cloudflare 能够更有效地路由数据包并缓存静态资源(如图像、JavaScript、CSS 等),从而加快页面加载时间。然而由于Cloudflare充当中间层,原始服务器在响应请求并记录日志时通常只能看到Cloudflare的IP地址,而非访问者的真实IP地址。

为了获取真实IP,需要对Nginx进行一些配置,

默认情况下会记录 Cloudflare IP 地址。原始访问者 IP 地址出现在附加的 HTTP 标头中,称为 CF-Connecting-IP

下面这个图很好的说明了,是否经过Cloudflare,以及是否设置远程IP 头的后端获取IP结果。

该图说明了使用和不使用 Cloudflare 处理 IP 地址的不同方式。

CF-Connecting-IP OR X-Forwarded-For

XFF X-forward-for 这个头可能大家也不陌生,但是在 Cloudflare 接入后,CF-Connecting-IPX-Forwarded-For 两个 HTTP 头字段都可以用于识别原始客户端的 IP 地址。

对于选择哪个IP作为后端的真实IP 需要对这两个头进行对比。

属性 CF-Connecting-IP X-Forwarded-For
头字段的设置 由 Cloudflare 设置 由每一层代理服务器设置或追加
包含的 IP 地址 单一的原始客户端 IP 地址 一个包含原始 IP 和代理链的 IP 地址列表
可信度 高,Cloudflare 独有,不易伪造 低,易被伪造或修改,依赖于所有代理的正确设置
适用场景 适用于经过 Cloudflare 的流量 适用于多层代理时,且所有代理都可信

### 推荐使用哪个?

综上在使用 Cloudflare 的情况下,推荐使用 CF-Connecting-IP 来获取客户端的真实 IP 地址

  1. 唯一性和简洁性CF-Connecting-IP 总是表示原始客户端的单一 IP 地址,简单直接,不会有多个 IP 地址链的问题
  2. 可信度高:由于 CF-Connecting-IP 是由 Cloudflare 直接提供的,它更可信,且不会被客户端伪造或修改。

正文

前面知道了 CF 传递的特有的 CF-Connecting-IP 头之后,我们就可以修改 Nginx 配置。让它把 CF-Connecting-IP 这个头包含的 IP 作为用户的真实IP。

下面是具体的配置过程

配置生成

我们编写脚本来进行配置文件的生成, 具体的逻辑是curl获取cloudflare 的边缘节点的IP列表。之后使用 set_real_ip_from 的配置,来信任这些源IP,来避免IP伪造的问题。

#!/bin/bash

CLOUDFLARE_FILE_PATH=/etc/nginx/conf.d/cloudflare.conf

echo "#Cloudflare" > $CLOUDFLARE_FILE_PATH;
echo "" >> $CLOUDFLARE_FILE_PATH;

echo "# - IPv4" >> $CLOUDFLARE_FILE_PATH;
for i in `curl -s -L https://www.cloudflare.com/ips-v4`; do
    echo "set_real_ip_from $i;" >> $CLOUDFLARE_FILE_PATH;
done

echo "" >> $CLOUDFLARE_FILE_PATH;
echo "# - IPv6" >> $CLOUDFLARE_FILE_PATH;
for i in `curl -s -L https://www.cloudflare.com/ips-v6`; do
    echo "set_real_ip_from $i;" >> $CLOUDFLARE_FILE_PATH;
done

echo "" >> $CLOUDFLARE_FILE_PATH;
echo "real_ip_header CF-Connecting-IP;" >> $CLOUDFLARE_FILE_PATH;

#test configuration and reload nginx
nginx -t && nginx -s reload

在我们运行这个脚本之后,会在 /etc/nginx/conf.d/cloudflare.conf 生成下面的配置。信任所有的cloudflare的边缘IP,并且设置 realip 为 CF-Connecting-IP 的头所标注的IP。

# 设置Real IP头
#Cloudflare ip addresses

# - IPv4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

# - IPv6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

# 设置使用的头字段
real_ip_header CF-Connecting-IP;

更新 Nginx 配置

  1. 打开 Nginx 配置文件

    Nginx的主配置文件通常位于 /etc/nginx/nginx.conf/etc/nginx/conf.d/default.conf,根据您的环境找到并编辑该文件。

  2. 添加 Cloudflare 真实IP头支持

    http 块或 server 块中,添加以下配置:

    include /etc/nginx/conf.d/cloudflare.conf;
    
  3. 保存并重启 Nginx

    保存配置文件,然后通过以下命令重启Nginx服务:

    sudo systemctl restart nginx
    

验证真实 IP 获取

查看访问日志

默认情况下,Nginx将访问日志保存在/var/log/nginx/access.log。可以使用以下命令查看日志:

tail -f /var/log/nginx/access.log

通过配置Nginx来正确识别Cloudflare传递的真实IP地址,在使用安全上可以有较大的提升。

而且应该是使用docker来部署的wp获取IP这种方式也是更加推荐的。不需要对容器内的内容进行修改。

Terraform使用列表来创建资源

作者 auto
2024年11月2日 00:00

在使用 Terraform 在中级的水平之后, 动态管理状态这点将会成为一个很重要以及很常用的特性。比如创建类似的资源的时候,就不在需要one by one 的创建对应的 resource 在配置文件中。

使用一个列表 或者 一个map 来对资源来进行管理,只需要在配置文件中体验出差分的配置内容即可。

在这篇文章根据几个实际过程中部署使用的例子来记录下如何使用动态配置。

通过map来批量批量创建主机

这段落如何使用 Terraform 定义一个 Map,利用这个 Map 来批量创建具有相同基础属性但个别属性不同的主机资源。通过这种方式,可以配置各个主机的系统盘、操作系统类型、可用区等属性。更重要的是,通过修改 Map 的内容即可动态增减主机数量,而无需修改 Terraform 配置文件本身,从而实现更高效的批量资源管理。

主机模板以及变量定义

此部分记录了一些预定义的配置和初始资源模板的设置。预定义的内容包括子网的选择和动态分配策略,例如可以将一批主机均匀或随机地分配到两个不同的可用区,以实现跨可用区的部署。

资源模板则定义了所需资源的全部属性,包括标签、系统盘容量、数据盘容量、IAM(身份与访问管理)属性和子网选择等配置。通过该模板,可以标准化资源的配置并确保在创建时符合预设的资源规范。

子网分配

首先,介绍如何实现主机在可用区中的均匀分布配置。由于使用 Map 来定义主机的属性,我们可以利用 index 参数来获取每个主机的索引值。因为有两个可用区,可以通过将索引值除以 2 来获取可用区列表的索引,从而实现主机在两个可用区中的均匀分布。

实际上,更好的方法是使用哈希函数等稳定分配的方案,但由于没有找到具体配置,这里只能使用 index。需要注意的是,Map 的索引值并不稳定,因此在增加或减少主机后,索引可能会发生变化。不过,由于子网并非频繁修改的属性,并且在 AWS 中直接修改子网可能导致主机的销毁,因此我们使用了 lifecycle 中的 ignore_changes 属性来忽略子网的变化。这样一来,在调整子网或增减主机时,不会导致其他资源被意外终止。

所以最终的配置如下所示,这样的话就可以实现一个简单的子网平均分配的方法

locals {
  node_secure_subnets = [aws_subnet.secure_subnet_a.id, aws_subnet.secure_subnet_b.id]
}

resource "aws_instance" "node_sec_template" {
  #...
  subnet_id              = local.node_secure_subnets[index(var.node_sec_instance_config, each.value) % 2]
  lifecycle {ignore_changes = [subnet_id]}
}

主机模板

可以看到这里的 for_each 会遍历一个映射(map)中的所有内容,然后根据每个元素的值填充到对应的资源字段中。例如,在下面的代码中,我们可以看到 tags 字段是根据 for_each 的内容动态生成的。

如果某个资源包含了另一个需要依赖的资源,比如 EBS_BLOCK_DEVICE,我们需要在主资源的定义中进行引用。通过这种方式,我们可以基于列表或映射来动态创建多个资源实例。

总结来说,通过 for_each 和列表,我们能够高效地生成动态的基础设施资源,比如动态创建多个主机实例,或者挂载 EBS 卷等。这种方式不仅减少了重复的配置代码,还大大提高了灵活性和可维护性。


resource "aws_instance" "node_sec_template" { # for_each = { for idx, config in var.node_sec_instance_config : idx => config if length(keys(config)) > 0 } for_each = local.node_config_map ami = each.value.ami instance_type = each.value.instance_type tags = { org = "xxx" project = "aaa" service = "Node" module = each.value.tags.Module Name = each.value.tags.Name Monitoring = "False" } # 系统盘配置 root_block_device { volume_size = each.value.root_volume_size volume_type = "gp3" tags = { Name = "root-${each.value.tags.Name}" } } # 数据盘配置 dynamic "ebs_block_device" { for_each = each.value.data_volume_size > 0 ? [1] : [] # 如果 data_volume_size > 0 才创建数据盘 content { device_name = "/dev/sdb" volume_size = each.value.data_volume_size volume_type = "gp3" delete_on_termination = false tags = { Name = "data-${each.value.tags.Name}" } } } iam_instance_profile = "role-user" key_name = "web-key" subnet_id = local.node_secure_subnets[index(var.node_sec_instance_config, each.value) % 2] vpc_security_group_ids = [aws_security_group.allow_outbound_all.id, aws_security_group.compiler_ssh_sg.id] lifecycle {ignore_changes = [subnet_id]} }

代码解析

  1. 主机信息映射
    通过 for_each,我们将 var.hosts 转换为一个键值对(map)。每个主机的名称作为键,主机的详细信息作为值。这样可以确保资源的名称是唯一的,同时方便资源之间的引用。
  2. 动态创建 EC2 实例
    aws_instance 配置中,for_each 遍历 hosts 列表,为每一个主机生成一个 EC2 实例,并使用主机对应的 ami
  3. 动态挂载 EBS 卷
    aws_ebs_volume_attachment 配置中,同样使用 for_each 遍历 hosts 列表。这里我们引用了实例的 ID,通过 aws_instance.example[each.key].id 动态关联 EBS 卷到正确的实例。
  4. 动态引用资源
    如果某个资源需要依赖另一个动态生成的资源,比如这里的 EBS 卷依赖 EC2 实例,我们可以使用 each.key 作为索引,精确地引用目标资源。

通过List来绑定目标组

在 Terraform 中,如果我们需要为每个 Target Group 绑定资源,直接为每个 Target Group 手动创建对应的绑定资源会显得非常冗余且繁琐。为了简化这一过程,可以利用列表(list)和 for_each 来实现批量绑定资源,从而避免重复的配置代码。

假设我们有一个包含多个 Target Group 的列表,每个 Target Group 都需要绑定到特定的目标主(Target Host)。以下是一个简化的例子:

var.tf 中定义一个列表来存储所有 Target Group 相关的信息,包括需要绑定的 ARN 和目标主的 ID:

locals {
  target_groups = [
    {
      target_group_arn = aws_lb_target_group.mine_alph.arn
      instance_id      = aws_instance.node_alph[0].id
    },
    {
      target_group_arn = aws_lb_target_group.mine_alph.arn
      instance_id      = aws_instance.node_alph[1].id
    }
  ]
}

通过 for_each 遍历上面定义的列表,为每个 Target Group 动态创建对应的绑定资源

# 定义目标组和实例的映射列表
# 使用for_each合并创建target_group_attachment
resource "aws_lb_target_group_attachment" "target_list_attach" {
  for_each         = { for idx, tg_map in local.target_groups : idx => tg_map }
  target_group_arn = each.value.target_group_arn
  target_id        = each.value.instance_id
  port             = 3333
}

代码解析

target_groups 列表

列表中的每个元素是一个映射,包含 Target Group 的名称(name)、ARN(arn)以及需要绑定的目标主 ID(target_id)。我们可以根据需求扩展这个结构,比如添加端口号或其他配置项。

for_each 遍历列表

在资源 aws_lb_target_group_attachment 中,for_each 会将列表转换成一个映射(map),以 name 作为键,整个元素作为值。这样,每个 Target Group 都会动态生成一个绑定资源实例。

动态引用

在资源配置中,each.value.arn 和 each.value.target_id 分别引用当前遍历元素的 arn 和 target_id。这样就避免了手动为每个 Target Group 写重复的绑定配置。

优势

简化配置
使用列表和 for_each 后,只需要定义一次资源逻辑,所有 Target Group 的绑定操作都可以自动完成。

灵活性高
如果需要新增一个 Target Group,只需要在列表中追加一个元素,而无需手动修改配置代码。

减少冗余
避免了为每个 Target Group 手动创建资源的繁琐工作,提升了代码的可读性和维护性。

在使用 TF 管理资源时,临配置重复和维护困难的问题,特别是当需要为一组相似的资源进行批量管理时。使用列表与 for_each 的结合就是最好的方式。这种方法不仅能提升开发效率,还能避免手动操作带来的错误。

IaC的最佳实践:让配置更具可读性、更易维护。也是从这个角度出发。

Terraform多Region的格式

作者 auto
2024年10月27日 00:00

Terraform 在基础设施的管理是是非常方便的。但是面对一个中型项目,管理文件的复杂度也会大幅提升。

特别是针对一个多regoin的项目,需要按regoin来进行拆分和管理,来获得更直观的资源展示。

这篇文章记录一下现在使用的项目结构,以便后续的复用

项目结构

➜  my-server tree .
.
├── eu-central-1
│   ├── ec2.tf
│   ├── lbs.tf
│   ├── main.tf
│   ├── security.tf
│   └── vars.tf
├── main.tf
├── set_env.sh
├── terraform.tfstate.d
│   ├── terraform.tfstate
│   └── terraform.tfstate.backup
└── us-east-1
    ├── ec2.tf
    ├── main.tf
    └── security.tf

项目结构如上面的目录树,每一个区域都拆分到了单独的文件夹中,对于多region的中小型项目,可以清晰的拆分各个区域的资源。

配置文件

在maintf 中,来设置 provider。通过 module 来引入,对应的模块的文件夹。

# Main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.63.1"
    }
  }
  backend "local" {
    path = "./terraform.tfstate.d/terraform.tfstate"
  }
}

module "us_east" {
  source = "./us-east-1"
}

module "eu_central" {
  source = "./eu-central-1"
}

在子文件夹中,就可以直接在main 里面设置provider 配置,这里设置的是region区域。

# us-east-1 main.tf
provider "aws" {
  region = "us-east-1"
}
# eu-central-1 main.tf
provider "aws" {
  region = "eu-central-1"
}

通过上面的模块引入之后,就可以使用,标准的方式来预定义一些变量了 。

variable "aws_vpc_id" {
    type = string
    description = "AWS VPC ID"
    default = "vpc-xxxx"
}

locals {
  vpc_info = {
    id = "vpc-0936ab715c6cee712"
    sub_private_a = "subnet-xxxx111"
    sub_private_b = "subnet-xxxx222"
  }
}


variable "public_subnets" {
  description = "List of Availability Zones"
  type        = list(string)
  default     = ["subnet-xxxxcccc", "subnet-xxxxdddd"]
}

以上记录一下,对于多region 的场景下如何来设计项目的目录结构来进行合理的划分和隔离。

这样避免了单个层级,堆叠了多区域/多文件 带来的管理混乱的问题。

记磁盘无法挂载 X.mount is bound to inactive

作者 auto
2024年10月29日 00:00

线上问题记录:Unit X.mount is bound to inactive unit

在这篇文章中记录一次线上环境中遇到的一个奇怪问题及其解决过程。问题发生在对新创建的磁盘进行挂载时。虽然通过 mount 命令显示挂载操作已成功,但使用 df -h 检查时却发现挂载并未生效。

然而,当尝试在另一个目录(例如 /mnt/D2)上进行挂载时,操作却可以顺利完成。

为了解决问题,进一步深入排查。发现问题似乎与 systemd 相关,因其未被重新更新而导致挂载无法正确生效。所以,写下这篇文章,旨在记录这个问题的发现和分析过程,尝试总结导致该问题的潜在原因。

定位问题

出现挂载问题之后,首先去检查了磁盘的状态,使用 lsblk df fdisk 等命令尝试去发现问题,但是看起来其输出内容都是正常的。排除了磁盘本身的问题。

熟悉使用 strace 来跑mount的命令,对比其具体的C调用是否有明显的问题。也没有发现明显异常。排除了系统调用过程中出现的问题。

尝试创建data2 使用命令挂载到 data2 发现成功挂载,怀疑挂载点问题,使用命令来检查 data 和 data2 的权限,以及特殊标志位的问题,均未发现其他的问题。

ls -ld /data
lsattr -d /data
stat /data

之后开始怀疑可能是系统内核态的问题,所以dmesg | tail -n20 查看内核日志。内容如下。

[20382.898799] pci 0000:00:1f.0: [1d0f:8061] type 00 class 0x010802
[20382.898912] pci 0000:00:1f.0: reg 0x10: [mem 0x00000000-0x00003fff]
[20382.899106] pci 0000:00:1f.0: enabling Extended Tags
[20382.899677] pci 0000:00:1f.0: BAR 0: assigned [mem 0xc0004000-0xc0007fff]
[20382.899773] nvme nvme3: pci function 0000:00:1f.0
[20382.899834] nvme 0000:00:1f.0: enabling device (0000 -> 0002)
[20382.915781] nvme nvme3: 2/0/0 default/read/poll queues
[20730.273270] EXT4-fs (nvme2n1): mounting ext3 file system using the ext4 subsystem
[20730.450819] EXT4-fs (nvme2n1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none.
[20856.064034] EXT4-fs (nvme2n1): mounting ext3 file system using the ext4 subsystem
[20856.241184] EXT4-fs (nvme2n1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none.
[20896.261646] EXT4-fs (nvme2n1): mounting ext3 file system using the ext4 subsystem
[20896.435957] EXT4-fs (nvme2n1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none.
[21223.746810] EXT4-fs (nvme2n1): mounting ext3 file system using the ext4 subsystem
[21223.921784] EXT4-fs (nvme2n1): mounted filesystem with ordered data mode. Opts: (null). 

但是,从实际上的内容看,mounted 代表着挂载是成功的,还是无法说明挂在失败的原因。继续检查日志查看 syslog

tail /var/log/syslog
Oct 28 09:22:32 ip-172-18-3-78 multipath: nvme2n1: uid = uuid.53290adb-9273-5530-a714-2ce51145e88e (sysfs)
Oct 28 09:23:14 ip-172-18-3-78 kernel: [24724.788270] EXT4-fs (nvme2n1): mounting ext3 file system using the ext4 subsystem
Oct 28 09:23:14 ip-172-18-3-78 kernel: [24724.924221] EXT4-fs (nvme2n1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none.
Oct 28 09:23:36 ip-172-18-3-78 systemd[1]: data2.mount: Succeeded.
Oct 28 09:23:36 ip-172-18-3-78 systemd[3564]: data2.mount: Succeeded.
Oct 28 09:24:01 ip-172-18-3-78 kernel: [24771.875507] EXT4-fs (nvme2n1): mounting ext3 file system using the ext4 subsystem
Oct 28 09:24:01 ip-172-18-3-78 systemd[1]: data.mount: Unit is bound to inactive unit dev-disk-by\x2duuid-3ffa8350\x2d323e\x2d4f1d\x2d9ae3\x2df751190774d2.device. Stopping, too.
Oct 28 09:24:01 ip-172-18-3-78 kernel: [24772.016299] EXT4-fs (nvme2n1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none.
Oct 28 09:24:01 ip-172-18-3-78 systemd[1]: Unmounting /data...
Oct 28 09:24:01 ip-172-18-3-78 systemd[3564]: data.mount: Succeeded.
Oct 28 09:24:01 ip-172-18-3-78 systemd[1]: data.mount: Succeeded.
Oct 28 09:24:01 ip-172-18-3-78 systemd[1]: Unmounted /data.

最终发现了异常所在,systemd 主动的把data 给umount 掉了,所以实际上是挂载成功,随即就被systemd 给卸载掉了。看到了 unit 的问题,所以经验上讲可能是 systemd 的mount uuid 状态不对导致,之后尝试

systemctl daemon-reload

之后重新挂载,挂载成功。并且 syslog 也没有 unmount 的日志出现了。问题解决。

为什么

在前面误打误撞的解决了这个挂载“失败”的问题,但是是什么原因导致的?

于是使用 Unit X.mount is bound to inactive unit 作为搜索关键词得到了下面的结论,又是一个 systemd 的锅,或者是说它多做了一些东西

Systemd 在启动时 会使用其自己的systemd-fstab-generator读取 /etc/fstab。相当于建立了一个缓存。

这意味着 Systemd 不会即时的 /etc/fstab 中的更改,因为生成器只运行一次,并且在系统启动后不会再运行,除非手动的进行reload操作。

systemd-fstab-generator 是一个生成器,它在启动初期以及重新加载系统管理器配置时将 /etc/fstab(有关详细信息,请参阅 fstab(5))转换为本机 systemd 单元。

这也意味着只有系统重启systemctl daemon-reload才会重新启动 systemd-fstab-generator 并重新读取 /etc/fstab。

在这个场景下,这台主机之前有一个已经挂载的磁盘,我把磁盘卸载之后重新挂载了新的镜像创建的磁盘。但是其fstab 信息对不上,uuid 之类的发生了改变。所以当我们手动挂载到同一个目录的时候,systemd 发现挂载的磁盘的信息与之前不一致,所以就出现了上面的inactibe 的问题。

参考

  • https://mamchenkov.net/wordpress/2017/11/09/systemd-strikes-again-unit-var-whatever-mount-is-bound-to-inactive-unit/
  • https://www.freedesktop.org/software/systemd/man/latest/systemd-fstab-generator.html
  • https://www.claudiokuenzler.com/blog/1124/linux-mount-not-working-systemd-unit-is-bound-to-inactive

[翻译]谷歌九年旅程的事后分析

作者 auto
2024年9月15日 00:00

微小的挣扎
一直在修修补补些什么。


我开始写这篇回顾时,正值在谷歌的最后一周,我已经结束了所有事情,并告别了同事们。作为一名前SRE(站点可靠性工程师),我觉得用事后分析的方式来总结这段经历会很有趣。

简介

我在加入谷歌时年纪还很轻,经验也相对不足,前后在谷歌工作了约9年。

我的软件之旅始于19岁,当时开始了第一份实习,之后一边继续完成应用物理学的学位,一边继续兼职或全职工作。在学位过程中,我对物理工作逐渐失去了兴趣,软件反而成了更有前途的职业方向。

某个时候,我被谷歌的一名猎头发现,最终这为我带来了在伦敦的实习机会,彼时我22岁,之后我全职加入了位于都柏林的谷歌。我参与了多个团队,专注于三个产品:Bigtable、持久磁盘(Persistent Disk)和谷歌云虚拟机(GCE VMs)。文末我附上了详细的时间线。

我对加入谷歌的期望是什么?

在伦敦实习期间,我初尝谷歌文化后,充满了期待。吸引我的是工程的深度,技术的复杂性以及工程师的水准。

在那之前,我只在波兰的小型软件公司和创业公司工作过,而谷歌……谷歌拥有世界上最好的技术之一,我有机会与之共事并学习,这让我非常兴奋。

谷歌给我留下了深刻的印象。我不认为自己会很快感到厌倦,我可能会在那里待上更长时间,也许甚至五年还会继续学习。除此之外,吸引我的还有薪水、福利和国际化的有趣社群。

从大局来看,我当时的目标是创办自己的公司,但在谷歌的这段“绕路”可以为我未来创业打下更好的基础,获得技能、乐趣和资金。所以,我决定一试。

那么,实际情况如何呢?

总体而言,这段经历非常精彩,有高潮也有低谷。期间有温暖人心的时刻、团队合作的快乐与满足,但也伴随着许多压力与挫折。

这是一场互利的交换,我投入了精力、心血和认知资源,而我从中得到了:

  • 丰厚的收入
  • 各种层次的工程技能,尤其是世界级的危机处理和调试能力
  • 领导力与管理技能
  • 身为一间酷公司的一部分的满足感
  • 很棒的公司外出活动和商务旅行

生活方式的福利也包括:

  • 办公室里的游泳池、世界级的健身房、各种运动课程、每周的按摩、健康或美味的餐饮、现场医疗护理
  • 优秀的社群与人际关系

为什么这个交换不再对我有吸引力了?

有几个因素影响了我的决定:

  • 达到了我的财务目标甚至超出了
  • 对谷歌技术的迷恋逐渐消退:
    • 行业技术开始赶上
    • 实际工作没想象中那么令人兴奋
    • 我的兴趣逐渐饱和或转移
  • 希望创办自己的公司(我可以等待,但不想等太久)
  • 公司变得不再那么有趣或酷
  • 预算削减(商务旅行减少,外出活动不再那么令人惊叹)
  • 裁员
  • 人员转移至廉价地区,导致我在本地的组织扩展机会减少
  • 安全和监管相关的繁文缛节增加
  • 复杂系统与团队间的关系导致工程挑战加剧,工作速度减慢
  • 认知负荷过大——尤其是在之前的角色中,谷歌的技术非常复杂,新员工可能需要一年时间才能完全适应,这有点疯狂
  • 个人成长机会不太明确
  • 没有明显的转变,只是更多的相似经历(虽然这种稳定性也有价值,但我不想停滞不前)
  • 职业发展达到瓶颈:L6是很高的技术职级,而我对L7不感兴趣,因为L7更偏向政治性的角色,而非个人工程角色。管理路线上的机会也有限……如果我有机会成为大规模组织的管理者,我或许会待得更久。
  • 技术工作的形态与我希望发展的方向不一致。

学到的经验

要将九年的学习总结成几句话是很难的。我学到了技术技能、软技能,变得更优秀、更智慧,也成为了一个更好的领导者。

作为一个(有点焦虑的)过度追求完美的人,我始终觉得自己可以做得更好,这也不断激励我努力学习和进步。

成功之处

  • 我升职速度很快
  • 最终达到了L6,这个职级很受认可且薪酬优渥
  • 我总是拥有很多自主权
  • 能够坚持工作与生活的平衡(合理的工作时间与工作量)
  • 激励人心的同事们,他们聪明且充满干劲
  • 赚到了超出我想象的钱
  • 享受了许多福利,过上了非常健康的生活方式
  • 有很多有趣的工作旅行(商务与外出活动)
  • 作为工程师和领导者都得到了成长
  • 软技能得到了极大的提升
  • 结交了许多工作上的朋友
  • 学到了许多有趣的技术
  • 有机会从零创建自己的团队
  • 以60%或80%的工作时间模式工作,这对我的生活方式和工作外的人际关系有极大帮助
  • 学会了处理(慢性)压力的许多方法

失败之处

  • 我在SRE岗位上待得太久——都柏林没有太多选择,而我也没有搬到其他地点(由于惰性、个人原因等)
  • 值班压力大,影响了我的睡眠
  • 这份工作与我的乐观、富有创造性的性格不太契合——这导致了某种程度上的失落感,我在工作中没有得到充分满足,而是通过外部项目来弥补
  • 24/7的工作性质使得很难完全脱离工作
  • 美国为中心的文化——如果你不在美国,而是处于谷歌的非中心地区,可能会感觉逆流而上,容易被孤立,或是被晚间会议压得喘不过气
  • 人员配置计划的变化——我曾两次被承诺团队扩展,最终都被取消了,或者以另一种形式再次承诺
  • 高级管理人员被事务淹没,未能提供支持、反馈或有效的监督(有时感觉有些像“狂野西部”)
  • 有很多时候,我感到被会议、重复性工作和资源不足的团队压得喘不过气,同时也没有好的工程或管理成长机会
  • 谷歌的认知负荷很高——需要记住无数系统和技术,它们可能以某种方式影响你的系统(在SRE岗位上尤其如此)

幸运之处

  • 谷歌的股票表现很好,加上我快速的职业轨迹,我的财务状况非常不错 🙂
  • 我招募的人表现非常出色
  • 尽管有些事情是具有挑战性的,但我仍然弄清楚了很多事情,并在高压力环境下取得了优异的表现
  • 我做出了良好的财务决策,尽管还可以更幸运,但这些决策背后有理性的思考过程
  • 我建立了很多优秀的人脉

我能做得不同的地方

  • 应该更早离开SRE岗位,因为从一开始我就知道这不是我想要的
  • 应该搬到其他办公室——人容易留恋已经拥有的好事,转换的成本也不小,但我过去低估了探索的重要性
  • 更好地利用教育补贴(例如,多参加斯坦福的在线课程)

下一步

接下来,我将进行至少6个月的休假,探索、放松、学习新事物,拓展自己对未来可以从事领域的认识。

我有一种倾向,即倾向于过早结束探索,同时热衷于以明确的目标进行生产性工作,因此休假对我来说是一种心理上的挑战。我会继续写关于这个主题的文章,敬请关注!


时间线

  • 2015年夏季:谷歌应用引擎SRE实习生(伦敦)
  • Cloud Bigtable SRE时期(都柏林)
    • 加入时为L3工程师
    • 9个月内晋升至L4
    • Cloud Bigtable团队负责人

    • 1.5年内晋升至L5
  • 持久磁盘SRE时期
    • 以L5级别加入
    • 游走于不同工作组,后来参与了一个后来成为Hyperdisk的项目
    • 美国姐妹团队解散,重建于西雅图
    • 成为Hyperdisk的临时技术负责人并组建了团队
    • 晋升至L6
    • 成为持久磁盘IO SRE的经理
    • 持久磁盘SRE遭遇了人员过度流失和倦怠问题(SRE招聘问题、开发组织迅速增长从40到200等)
  • GCE Fleet维护开发时期
    • 以L6级别的个人贡献者加入
    • 招聘我的主管晋升为副总裁,将我交给了一位过于繁忙的其他经理
    • 接手了一个内部挣扎中的项目,使其成功,并在都柏林组建了一个团队
    • 从个人贡献者转为管理者,管理4人,后升至6人
    • 在另一位高级经理手下启动了第二个团队
    • 由于人员配置不足,重组了第二个团队,并重新定义了主要团队的任务

通过这篇文章,我分享了在谷歌的九年旅程中的一些关键经历和反思。对于未来,我希望能够更多地探索和学习。


如何设计一个可持续性的个人云

作者 auto
2024年9月20日 00:00

构建一个个人云不仅是一种趋势,更是一种保证数据隐私和掌控权的必要手段。

然而,大多数情况是我们开一台虚拟机,玩玩之后随他过期上面的东西也都没了。这些部署都是不可持续的 unsustainable

在我思考了曾经哪些玩玩就扔掉的机器之后,得出结论:

在设计一个可持续的个人云时,有三个关键特性:整体性便利性隐私性

这些特性直接决定了你的个人云能等持续运行下去。

整体性:高内聚方便迁移

个人云的架构必须要有灵活性和可迁移性。一个设计良好的个人云,应该具备简易迁移的能力。当环境或设备发生变化时,比如从家庭服务器迁移到云端,或服务器过期迁移。所以个人云的迁移过程应当尽可能简便。

这不仅意味着配置和数据能够快速同步和恢复,还要保证系统架构具有足够的可扩展性

整体性是我们个人云的运行关键特性,无论环境如何改变,我们的系统都可以快速迁移。

便利性:随时随地连接使用

对于个人云而言,便利性意味着用户能够在任何时间、任何地点轻松访问其服务。

无论是从家里、办公室,还是在外地旅行,个人云的访问应该是稳定、流畅的。

而且,用户不应当花费过多时间去处理复杂的网络设置和配置问题。

实现这种便利性的关键是降低访问门槛,减少用户手动配置的步骤。这包括使用反向代理、自动化的网络配置工具

好的个人云设计,应该几乎不到阻隔的存在,只需通过设备就能直接访问需要的数据或应用。

隐私性:数据加密传输

隐私性也是很关键的一点。随着个人数据虽然价值密度低,但是对于个人而言还是很重要的。

所有数据在存储和传输过程中都应当经过加密处理,以防止未经授权的访问。

在设计个人云时,通信加密和身份验证机制必须被集成到架构中。


解决方案工具简介

前面分析了一个可持续话个人云的重要的三点要求,下面就是软件架构方案

软件方案的详细介绍

为了实现这些特性,下列工具被用作实际方案:

  • Tailscale: 提供私有 VPN 服务,在多主机/VPS之间快速建立安全的网络连接,实现设备间的高速互联,家庭网络/手机客户端的ipv6也可以帮助获取低延迟的体验(同省内直连 ping < 20ms)
  • Cloudflare Tunnel: 通过一键反向代理+隧道的方式,为 self-hosted 应用提供简化的访问路径,减少网络配置的复杂性,提升便利性。
  • Cloudflare Zero Trust Application: 一键配置提供敏感应用的身份验证功能,确保外部访问的安全性,保护隐私和数据加密传输。

通过这些工具的组合,能够有效地满足个人云的三大核心需求,构建出一个可持续、易用且安全的个人云系统。

在设计个人云时,选择合适的工具是确保系统高效、可靠和安全的关键。以下是用于实现前述三大核心特性的具体软件方案:

1. Tailscale

Tailscale 提供了一种灵活的 VPN 服务,专注于简化主机之间的互联。通过 Tailscale,用户可以利用内网 IP 直接访问各个设备,无需暴露主机的外部端口,也无需担心公网非加密的流量。

Tailscale 的主要优势在于,它基于 WireGuard 协议,提供了高效的点对点连接,并且自动处理防火墙穿透和 NAT 问题。用户只需轻松配置,即可实现不同主机间的高速互联,无论设备位于家庭网络、云端还是外网,访问都像在同一个局域网中一样简单。这不仅极大提升了整体性和可迁移性,还降低了维护成本。

2. Cloudflare Tunnel

Cloudflare Tunnel 提供了简单、快速的反向代理和隧道服务,它允许用户在不开放任何主机端口的情况下,将本地服务暴露给互联网。其最大的优势在于,可以通过 Cloudflare 的基础架构快速部署一个支持 HTTPS 加密的站点,而无需配置证书或防火墙规则。

通过 Cloudflare Tunnel,可以让外部用户以安全的方式访问自托管应用,例如个人文件存储系统、博客平台等。Cloudflare 负责处理流量的加密和认证,用户无需担心数据在传输过程中的安全性。这种自动化的部署方式,减少了配置复杂度,极大地提升了便利性。

但问题是中国地区的主机会默认连接到 SJC 也就是 San Jose, CA, United States,速度相当慢

3. Cloudflare Zero Trust Application

Cloudflare Zero Trust Application 提供了对任意域名的强大鉴权功能。通过简单的配置,用户可以为任何外部应用建立一个安全网关,并通过多种身份验证机制(如 TOTP 或 GitHub OAuth)保护应用的访问。

这一鉴权网关可以用于保护个人云中敏感的后台管理域名,确保只有经过认证的用户才能访问。

例如,使用 TOTP(时间一次性密码)或 GitHub 的 OAuth 身份验证,个人云中的应用和服务将得到多层次的保护,进一步增强了隐私性。关于部署流程可以参考

软件部署

TBD


应用场景示例

可以实现多个自托管应用和服务的高效部署。以下是一些常见的应用场景:

  1. 个人文件同步与备份
    • 使用 Tailscale 让家中的 NAS 设备与笔记本电脑建立 VPN 连接,无论身在何处都可以通过内网 IP 快速访问文件。通过 Cloudflare Tunnel,可以将文件存储系统如 Nextcloud 暴露到互联网上,实现随时随地的文件访问。
  2. 自托管博客或网站
    • 通过 Cloudflare Tunnel,可以快速将自托管的博客(如 Ghost 或 WordPress)暴露给互联网用户,并自动获得 HTTPS 加密保护。借助 Cloudflare Zero Trust Application,还可以为博客的管理后台设置 TOTP 验证,避免后台受到恶意攻击。
  3. 远程桌面与开发环境
    • 使用 Tailscale,将家庭服务器或工作站与笔记本电脑连接,随时远程访问开发环境。无需担心端口暴露或公网访问的安全问题,所有流量都经过加密隧道处理。
  4. 个人任务管理与笔记
    • 使用 Cloudflare Tunnel 将自托管的任务管理工具(如 Kanboard)或笔记工具(如 Joplin Server)部署到互联网上,随时通过加密连接访问和管理任务、笔记。Zero Trust 可以为这些应用添加额外的身份验证层,确保敏感信息的安全性。

通过这些工具和应用场景,可以构建一个高效、安全、可持续的个人云,既满足日常使用需求,又确保了未来的可扩展性和数据隐私。

FIRE是什么?为什么要FIRE?

作者 auto
2024年9月15日 08:00

在当今社会,财务自由和提早退休(Financial Independence, Retire Early,简称FIRE)正成为越来越多人追求的目标。FIRE运动强调通过积累足够的财富,实现提早退休,并拥有更大的自由去选择如何生活。为了更好地理解FIRE,这篇文章将介绍什么是FIRE,为什么越来越多人选择这条道路,以及它对个人生活的影响。

正文

什么是FIRE

FIRE 是一种理财哲学和生活方式,旨在通过节省和投资积累足够的资产,以便在相对较年轻的年龄实现财务独立,并选择是否提早退休。FIRE的核心理念是通过减少不必要的开支和增加储蓄率,投资于高回报的资产,从而使你不再需要依赖工资收入来维持生活。

FIRE的核心要素

  • 财务独立(Financial Independence): 通过存钱和投资,积累足够的财富,以便在不工作或选择性工作的情况下,依然能够维持理想的生活水平。
  • 提早退休(Retire Early): 一旦实现财务独立,就可以选择在传统退休年龄之前停止全职工作,实现“提早退休”。

FIRE的基础理念

要理解为什么越来越多人选择FIRE,需要了解其基础理念:

  • 高储蓄率: FIRE的倡导者通常会保持高储蓄率,通常建议将收入的50%以上用于储蓄和投资。
  • 有效投资: 通过投资股票、债券、房地产等资产来实现财富的增值,使得财富积累速度快于通货膨胀率。
  • 简化生活: 控制支出和减少不必要的消费,以实现更高的储蓄率和减少财务压力。
  • 创建被动收入: 通过投资产生被动收入,使其可以替代工作收入。

FIRE 通常类型

FIRE 通常分为以下几种类型:

  1. Fat FIRE:在不降低生活质量的前提下,积累大量财富以实现财务自由。
  2. Lean FIRE:通过极简主义生活方式和低开支,实现财务独立。
  3. Barista FIRE:积累一定财富后,通过兼职或低强度工作继续维持生活,同时享受财务自由带来的灵活性。
  4. Coast FIRE:早期大量储蓄并投资,使得后期只需维持基本的生活开支即可实现财务独立。

为什么要选择FIRE

越来越多人选择FIRE,主要原因如下:

  1. 自主生活:FIRE 让人们拥有更多的时间和自由去追求他们真正热爱的事情,无论是旅行、创业、学习新技能,还是与家人共度时光。
  2. 心理健康:工作压力和职业倦怠问题日益严重,FIRE 提供了一种逃离传统工作模式的途径,减少精神压力,提升生活质量。
  3. 经济安全:通过积累财富和投资,FIRE 提供了经济保障,减少了对单一收入来源的依赖,增加了应对突发事件的能力。
  4. 环境影响:FIRE 通常与节约和环保生活方式相结合,减少浪费和过度消费,有助于实现更可持续的生活方式。

FIRE 对个人生活的影响

实现FIRE 不仅仅是财务上的自由,它对个人生活的影响是深远的:

  1. 时间管理:FIRE 实现后,你将拥有更多的时间来管理自己的人生,无需再按固定的工作时间表生活,可以更灵活地安排每天的活动。
  2. 人际关系:更多的自由时间可以用来与家人和朋友共度美好时光,增强人际关系和社会连接。
  3. 健康生活:有更多时间和资源关注健康,可以进行更多的运动和健康饮食,从而提高整体健康水平。
  4. 自我实现:FIRE 提供了一个平台,让你去探索和实现自己的兴趣和梦想,无论是写作、艺术、音乐,还是其他创意活动。

FIRE适合每个人吗?

虽然FIRE看起来很吸引人,但并不适合每个人。

它需要强烈的纪律性、良好的财务规划和一定的牺牲(如减少非必要支出)。

此外,每个人的财务状况、职业发展、家庭责任和生活目标各不相同,所以在决定是否追求FIRE之前,需要深入思考个人的生活目标和财务状况。

FIRE是一种希望通过提早积累财富,实现财务自由并提前退休的生活方式。它不仅仅是一种财务规划方法,更是一种追求更高自由度和生活质量的理念。通过了解FIRE的意义和优势,可以更好地判断这种生活方式是否适合自己,并做出最合适的选择。

使用 Cloudflare ZeroTrust保护你的后台页面

作者 auto
2024年9月13日 00:00

最近在重整站点的时候,看到wordpress后台一直在被尝试密码暴破,虽然做了IP级别的登陆尝试限制,但是感觉还是没有十足的安全感。为了解决这个问题,使用 Cloudflare Zero Trust 的application 方案,为 WordPress 后台登录页面增加一层保护,确保只有授权用户能够访问和操作。

试试看?👉 我的管理后台

image-20240913193414186

正文

Zero Trust 的使用优势

Zero Trust(零信任)是一种网络安全模型,假设网络内外的任何人或设备都不可信,并需要验证每一个试图访问资源的请求。这种方法不仅有效防止了外部的攻击,也能限制内部的威胁。通过为 WordPress 后台页面启用 Cloudflare 的 Zero Trust 服务

  • 访问控制:确保只有经过授权的用户才能访问后台页面,有效防止暴力攻击和其他未经授权的访问。
  • 多重认证:可以为登录页面添加多因素认证(MFA),进一步提高安全性。
  • 活动监控:实时监控所有访问请求,能够快速识别和阻止可疑活动。

配置 Cloudflare Zero Trust 保护

如果域名托管在 Cloudflare上面,那就可以进行进行直接的接入,在五分钟拥有一个 有保护的后台

1. 配置认证方式

在左边栏 Settings -> Authentication,找到认证方式的设置,在下面点击Add new。

image-20240913212534170

这里我添加了两种认证方式,一种是邮件的 TOTP就是给你邮箱发送一个一次性验证码,另一种是使用 github来进行oauth来登陆。具体配置点击Edit 有详细介绍

2. 创建 应用

  1. 登录到 Cloudflare 仪表板

    进入 Cloudflare 仪表板后,选择所需的网站,导航到 Zero Trust 菜单下的 Access

  2. 创建应用程序

    Access 页面中,选择 Applications,点击 Add an application 按钮。

  3. 设置应用程序的基本信息

  • Application Name(应用名称):输入名称,例如 “WordPress Admin”。
  • Subdomain(子域名):输入后台子域或路径,我的是 wordpress ,所以我这里是 例如 “/wp-login.php”。
  1. 选择应用类型

    在应用类型中,选择 Self-hosted(自托管),因为 WordPress 是运行在自己服务器上的。

之后点击保存,即可完成这个APP的创建,党完成创建之后这个路由已经托管在了CF上,当用户访问的时候需要先进行认证之后才能进入真正的 wordpress 后台。

3. 验证配置

在打开后台的时候可是看到直接跳转到验证登陆界面。而且从wp的后台来看,密码爆破攻击已经不在发生。

image-20240913213226088

通过使用 Zero Trust 服务,可以显著提高后台页面的安全性,防止未经授权的访问和潜在的攻击。

Nignx获取Cloudflare的真实IP

作者 auto
2024年9月13日 08:00

网站流量通过 Cloudflare 网络路由时,Cloudflare 作为反向代理来处理用户请求。Cloudflare 能够更有效地路由数据包并缓存静态资源(如图像、JavaScript、CSS 等),从而加快页面加载时间。然而由于Cloudflare充当中间层,原始服务器在响应请求并记录日志时通常只能看到Cloudflare的IP地址,而非访问者的真实IP地址。

为了获取真实IP,需要对Nginx进行一些配置,

默认情况下会记录 Cloudflare IP 地址。原始访问者 IP 地址出现在附加的 HTTP 标头中,称为 CF-Connecting-IP

下面这个图很好的说明了,是否经过Cloudflare,以及是否设置远程IP 头的后端获取IP结果。

该图说明了使用和不使用 Cloudflare 处理 IP 地址的不同方式。

CF-Connecting-IP OR X-Forwarded-For

XFF X-forward-for 这个头可能大家也不陌生,但是在 Cloudflare 接入后,CF-Connecting-IPX-Forwarded-For 两个 HTTP 头字段都可以用于识别原始客户端的 IP 地址。

对于选择哪个IP作为后端的真实IP 需要对这两个头进行对比。

属性 CF-Connecting-IP X-Forwarded-For
头字段的设置 由 Cloudflare 设置 由每一层代理服务器设置或追加
包含的 IP 地址 单一的原始客户端 IP 地址 一个包含原始 IP 和代理链的 IP 地址列表
可信度 高,Cloudflare 独有,不易伪造 低,易被伪造或修改,依赖于所有代理的正确设置
适用场景 适用于经过 Cloudflare 的流量 适用于多层代理时,且所有代理都可信

### 推荐使用哪个?

综上在使用 Cloudflare 的情况下,推荐使用 CF-Connecting-IP 来获取客户端的真实 IP 地址

  1. 唯一性和简洁性CF-Connecting-IP 总是表示原始客户端的单一 IP 地址,简单直接,不会有多个 IP 地址链的问题
  2. 可信度高:由于 CF-Connecting-IP 是由 Cloudflare 直接提供的,它更可信,且不会被客户端伪造或修改。

正文

前面知道了 CF 传递的特有的 CF-Connecting-IP 头之后,我们就可以修改 Nginx 配置。让它把 CF-Connecting-IP 这个头包含的 IP 作为用户的真实IP。

下面是具体的配置过程

配置生成

我们编写脚本来进行配置文件的生成, 具体的逻辑是curl获取cloudflare 的边缘节点的IP列表。之后使用 set_real_ip_from 的配置,来信任这些源IP,来避免IP伪造的问题。

#!/bin/bash

CLOUDFLARE_FILE_PATH=/etc/nginx/conf.d/cloudflare.conf

echo "#Cloudflare" > $CLOUDFLARE_FILE_PATH;
echo "" >> $CLOUDFLARE_FILE_PATH;

echo "# - IPv4" >> $CLOUDFLARE_FILE_PATH;
for i in `curl -s -L https://www.cloudflare.com/ips-v4`; do
    echo "set_real_ip_from $i;" >> $CLOUDFLARE_FILE_PATH;
done

echo "" >> $CLOUDFLARE_FILE_PATH;
echo "# - IPv6" >> $CLOUDFLARE_FILE_PATH;
for i in `curl -s -L https://www.cloudflare.com/ips-v6`; do
    echo "set_real_ip_from $i;" >> $CLOUDFLARE_FILE_PATH;
done

echo "" >> $CLOUDFLARE_FILE_PATH;
echo "real_ip_header CF-Connecting-IP;" >> $CLOUDFLARE_FILE_PATH;

#test configuration and reload nginx
nginx -t && nginx -s reload

在我们运行这个脚本之后,会在 /etc/nginx/conf.d/cloudflare.conf 生成下面的配置。信任所有的cloudflare的边缘IP,并且设置 realip 为 CF-Connecting-IP 的头所标注的IP。

# 设置Real IP头
#Cloudflare ip addresses

# - IPv4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

# - IPv6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

# 设置使用的头字段
real_ip_header CF-Connecting-IP;

更新 Nginx 配置

  1. 打开 Nginx 配置文件

Nginx的主配置文件通常位于 /etc/nginx/nginx.conf/etc/nginx/conf.d/default.conf,根据您的环境找到并编辑该文件。

  1. 添加 Cloudflare 真实IP头支持

http 块或 server 块中,添加以下配置:

include /etc/nginx/conf.d/cloudflare.conf;
  1. 保存并重启 Nginx

保存配置文件,然后通过以下命令重启Nginx服务:

sudo systemctl restart nginx

验证真实 IP 获取

查看访问日志

默认情况下,Nginx将访问日志保存在/var/log/nginx/access.log。可以使用以下命令查看日志:

tail -f /var/log/nginx/access.log

通过配置Nginx来正确识别Cloudflare传递的真实IP地址,在使用安全上可以有较大的提升。

而且应该是使用docker来部署的wp获取IP这种方式也是更加推荐的。不需要对容器内的内容进行修改。

导入现有资源到Terraform

作者 auto
2024年9月9日 00:00

记录下用于记录和分享,导入现有资源到 terraform 的最佳实践。

这是terraform 在使用过程中很常见的一种做法,但是在做的过程有些小细节可以没有注意。所以写这边文章来记录和分享一下。

正文

Terraform 管理资源的基本原理是保持远程的资源数据和本地的状态文件一致,所以我们导入资源就是把远程的状态复制到本地

这里假定你已经有了一个 初始化后的 terraform的项目

本地创建空资源

  • 在本地创建空资源块来接收导入的状态

空的 aws_ssm_parameter 资源块,将在导入时自动填充,这里以 aws_iam_policy 为例子。

确认资源的命名需要通过 terraform的文档来进行查询,文档提供了相当全面的参数。

那么aws_iam_policy 对应的空结构如下。

resource "aws_iam_policy" "user_limit_access" {
  # 参数将在导入之后根据导入的内容进行调整
}

导入远程状态

导入方法有两种,在高版本的 terraform 中有良好的支持

  1. import block
  2. Cli

在这里看实际情况选择使用,如果是少数资源使用 CLI 进行命令执行即可,但是如果导入资源数量较多,推荐使用 block形式进行导入。这里两种方式都会列举

Block 形式

import {
  to = aws_iam_policy.administrator
  id = "arn:aws:iam::123456789012:policy/UsersManageOwnCredentials"
}

Cli 形式

terraform import aws_iam_policy.administrator arn:aws:iam::123456789012:policy/UsersManageOwnCredentials

执行命令后 Terraform 会通过API来拉取资源的远程状态同步到本地,提示 Successful

编辑资源与本地状态一致

Terraform 目的是让资源文件定义的状态与远程一致。所以现在我们需要修改 tf文件让它的配置和 state中保持一致。这样就实现了同步,而不会做任何的变更。

使用下面命令列出现在所有的资源文件的详细状态

terraform show | less

并且进行资源搜索,找到它并且复制。在本次演示中,资源的状态内容如下:

resource "aws_iam_policy" "ssm_user_limit_access" {
  name = "ssm-user-limit-access"
  path = "/"
  policy = jsonencode(
    {
      Statement = [
        {
          Action = "ssm:StartSession"
          Condition = {
            Bool = {
              "aws:MultiFactorAuthPresent" = "true"
            }
          }
          Effect = "Allow"
          Resource = [
            "arn:aws:ec2:ap-east-1:0000:instance/i-kkkkkk",
            "arn:aws:ssm:ap-east-1:0000:document/SSM-SessionManagerRunShell"
          ]
          Sid = "VisualEditor0"
        },
        {
          Action   = "ssm:TerminateSession"
          Effect   = "Allow"
          Resource = "arn:aws:ssm:*:*:session/$${aws:username}-*"
          Sid      = "VisualEditor1"
        },
      ]
      Version = "2012-10-17"
    }
  )
  tags     = {}
  tags_all = {}
}

复制状态信息到 资源的tf文件中。

验证 Terraform 配置

在完成配置之后,运行以下命令来验证 Terraform 配置是否正确:

terraform plan

如果一切正常,你将会看到计划输出。如果是 No changes. Your infrastructure matches the configuration. 那就说明我们的本地状态与远程的完全一致没有变化。就完成了这个资源的导入

总结

一个 导入现有的资源到本地的 SOP,对于terraform 的工作原理的理解也会有所帮助。

❌
❌