2026-05-03 08:00:00
这几年看发布会、英美剧和电影时,我偶尔会暂停画面,把一句话摘下来。一开始只是觉得写得好,后来才意识到,它们之所以能被我记住,并不只是因为表达漂亮,而是因为它们在某个时刻说中了我正在经历、正在怀疑,或者一直说不清楚的东西。
语言即世界。但语言不是世界本身,它决定了世界以什么方式被看见。很多经验在被说出来之前,只是一团模糊的感受;一旦被某个准确的句子命名,它就从背景里浮出来,变成可以辨认、可以反复审视的东西。
这些句子后来慢慢从剧情里脱落出来,变成了我理解现实的某种参照。它们改变世界观的方式并不剧烈,更像是给原本模糊的经验加了一层轮廓:原来技术进步已经被视为日常、亲密关系会这样消耗、金融系统在进行系统性排他...
下面只摘英文原文。中间穿插的部分并不是翻译。
I’m going to go rogue for a minute. You guys get it but sometimes people take technology for granted. And just for perspective, I’m mic’d -- and in fact I’m actually double mic’d in just the right location so you can hear me. Deirdre is out in the middle of a windy lake and the only microphone Deirdre has is the little tiny one on the Apple Watch. It’s a foot or two away from her mouth, she’s paddling, and the signal’s being sent over cellular, coming in, and that’s just darn close to magic. Who would have thought?
来源:Jeff Williams,Apple 2017 秋季新品发布会
After thousands of years of compounding scientific discovery and technological progress, we have figured out how to melt sand, add some impurities, arrange it with astonishing precision at extraordinarily tiny scale into computer chips, run energy through it, and end up with systems capable of creating increasingly capable artificial intelligence.
来源:Sam Altman,《The Intelligence Age》
这两段话讲了同一件事:技术一旦稳定地进入日常生活,就会迅速从最初的奇迹退化成熟悉到可以忽略的背景。我们习惯它、挑剔它,把它视为理所当然,甚至认为它本来就应该存在。
Jeff Williams 在 Apple 发布会上的那段临场感叹,真的很有意思:他短暂地打断了发布会惯常的产品叙事,把一个 demo 重新解构成了工程奇迹 —— 一位 Apple 员工在风大的湖面上划船,手腕上戴着一块小表,靠一个离嘴很远的麦克风,再通过蜂窝网络把声音实时传回会场。Sam Altman 说「人类把沙子熔化、掺入杂质、在极小尺度上精密排列、通上电,最终得到能够创造人工智能的系统」。
我们生活在大量被驯化的魔法之中,只是它们被包装成了产品、参数、套餐、更新日志和下一代卖点。技术进步最容易被消费主义叙事掩盖。真正值得惊讶的不是新品又多了一个功能,而是工程能力如何一点点把过去不可想象的事情变成默认选项。
He says my fund packages itself as some sort of crusader for market efficiency and societal good. I make no claims to that effect. I’m a money manager. My only operating software is the profit motive. That and my id. They’re probably indivisible now, bleeding into every aspect of my life, for good and bad, often very bad. However bad you want to tell the audience I am, let me tell them: I’m worse. So make your judgment call.
来源:Eric Tao,《投行风云》S04E06《Dear Henry》
投行风云里少有的精彩独白。这句话的背景其实很复杂,Eric Tao 当时的处境其实非常糟糕,但是还是愿意在电视直播辩论上面对自己的做空的公司的董事长放出这样的狠话。这句话狠的地方不在于一位金融从业者承认自己追求利润,而在于他拒绝把利润动机包装成像类似浑水公司那样更高尚的公共叙事。他没有辩解,也没有洗白,只是把金融世界里那层常见的道德外衣撕掉了。
一个系统最真实的样子,往往不在它宣称自己相信什么,而在它的激励机制最终奖励什么。
Let me explain something to you, in your long life, you have not yet had occasion to understand: Friendly relationships are dangerous. They lend themselves to ambiguities, misunderstandings and conflicts, and they always end badly. Formal relationships, on the other hand, are as clear as spring water. Their rules are carved in stone. There’s no risk of being misunderstood and they last forever. Now, you need to know: I do not appreciate friendly relationships but I’m a great admirer of formal ones. Where there are formal relationships there are rites and where there are rites the earthly order reigns.
来源:Lenny Belardo,《年轻的教宗》第一季第一集
所谓友好的关系,常常因为边界模糊而制造误会;反而是正式关系,因为规则清楚、距离明确,能让彼此少一些猜测。这当然不是说人应该拒绝亲近,而是「熟」这件事本身并不天然可靠。熟悉有时会带来信任,有时也会带来僭越。很多关系一旦缺少形式,责任、边界和分寸就会变成只能靠默契维持的东西,而默契是很脆弱、可遇不可求的。
形式过去总显得繁琐、疏离和低效。后来才慢慢看出另一层价值:很多必要的形式其实是在替人节省情绪成本。它把不能说出口的边界,提前写进了规则里。
"Mortgage backed securities", "Sub-prime loans", "Tranches"... is pretty confusing, right?
Doesn’t it make you feel bored... or stupid?
Well... it’s supposed to. Wall Street loves to use confusing terms to make you think only they can do what they do, or even better, for you just to leave them the fuck alone.
来源:《大空头》
《大空头》里 Margot Robbie 洗澡时面对镜头打破第四面墙的解释,后来成了我经常主动探究各行各业专业术语的一个重要起点。术语当然有必要存在,因为复杂系统需要精确表达。然而,术语同样可以被用来刻意制造距离,制造权威,制造服从。
很多行业都有类似的语言机制。它先让外行感到困惑,再让外行因为困惑而自我怀疑,最后让外行把判断权主动交出去。金融只是其中最典型、也最昂贵的一种。
从那以后,「听不懂」这件事少了一点羞耻感,多了一些好奇心。听不懂并不总是因为自己笨,也可能是因为对方没有动机/义务让你听懂。面对任何复杂叙事,最重要的问题不是先承认自己无知,而是追问:这个复杂性是真的来自事实本身,还是来自某种刻意维护的信息不对称?
Pretty much of it...
以后再看到类似的句子,应该还会继续摘下来。
它们像一些很小的钉子,把某段时间里的疑问固定住。过几年回头看,剧情、角色,甚至当时为什么被击中,也许都会变得模糊;但这些句子留下来的那一点偏移,会继续存在于我看待世界的方式里。
2025-12-31 08:00:00
互联网行业从业六七年了,最关注的必然是养活自己的 Java 生态。很多人说 Java 很臃肿,写个 Hello World 都费劲。然而实际上只需稍微关注一下最新今年 9 月 Java 25 LTS 最新版本的语法:
void main() {
IO.println("Hello Java 25!");
}
虽然 JEP 445 这种类似脚本的语法糖代码已经脱离了一切皆对象的原则,但这恰恰是 Java 与时俱进的标志。另外几个不得不提的:结构化并发,虽然仍然在预览阶段,等它 GA 很有必要好好聊聊,配合虚拟线程,Java 的多线程模型简直起飞;模块导入声明简化了大量 import 语句,自 Java 9 以来的模块化的好处初见成效了。
其他领域的观察/观点:
我自己关于 AI 的暴论:
上一份工作在 SaaS 行业,属于人力资源行业内领头公司的老员工出来创业组建的一只小团队。总结下来的感受,SaaS 在国内很难做。由于是新项目,客户本就不多,最后几个月需求明显骤减,年中果然吃了散伙饭。虽然很快就换了工作,但每次想起那些代码写下后没有回响,仍然很难释怀。当然创业公司的好处就是很自由,公司架构精简,老板兼职商务销售经常出差,产品兼职人事/测试,不打卡,不用和外部客户沟通,节假日也几乎不会出现紧急线上问题,或许是因为没业务:(。我来到上海第一年做在线教育也是类似的团队。可能我自己一直喜欢从零到一构建点东西,所以更偏向这类创业公司的氛围。
换了工作后,在新公司从事互金相关业务,工作性质和公司规模导致部分福利或多或少不如先前,开发需求也多出了不少。但新公司有一定规模的业务,节奏稳定、方向明确,每个人只需要把自己负责的那一条脉络梳理好即可。
从个人成长的角度看,这样的环境也并非没有价值。系统规模更大,历史包袱更重,也意味着需要更谨慎地做决策、更耐心地理解上下游逻辑。现在看来,开发在多数时候并不是用来「创造」,而是用来「维持」:保证稳定、避免风险、在有限的空间内做最优解,这本身也是一种能力。只是偶尔在需求间隙,还是会不自觉地回想起以前那些可以随意推翻重来的日子。那时写代码,更多是在回答「能不能这样设计」;而现在,更多是在权衡「应不应该这么改」「如何避免改动的破坏性影响」。前者更像是在白纸上画草图,后者则是在维护一栋已经上路的汽车,各有意义,也各有约束,无可厚非。
或许职业生涯本就会在不同阶段切换角色:有时是开荒的人,有时是守成的人。眼下这段经历,大概更接近后者。至于未来是否还会再回到从零开始的状态,目前还很难下结论。但至少在当下,把手头的事情做好,理解这套体系为何如此运转,也未尝不是一种积累。毕竟,很多判断只有在真正站到另一种位置上,才能看得更清楚。
年初用国补还买了小米电视 S55 MiniLED,但今年都基本没怎么剧和电影,可以说是最近十年看的最少的一年了。每周必看 B 站主要的几个 Up:小 Lin 说,Koala 聊开源,马督工,jyhachi。
2026 年期待的剧集:豺狼的日子 第二季,投行风云 第四季。
用 Zed Vibe Coding 了一个页面,罗列了最近几年买的主要电子设备。AI 就是用来写这种东西快准狠。
我的 IDE 除了 JetBrains 之外现在用的就只有 Zed 了。Atom 项目原班人马的新作品,非 Java 项目可以全用 Zed,原生支持 Vim 操作。就图一个简单清爽内置 AI。 Qoder JetBrains Plugin 插件做的蛮好的,我也在用,但 JetBrains 自家的 AI Assistant 是真的拉垮,BYOK 完全不可用。
Postman 最烂的地方在于它作为一个 HTTP 客户端搞出一堆依赖自家网络的用户功能,第一天用它的时候就很讨厌,工作原因还不得不去用它。
老婆给我买了一款松下剃须刀,能用,但声音巨大,操作交互非常不友好,对不起它大几百的价格,找官方解决结果还要返厂检修。我可怕麻烦了,讨厌一个品牌就是这么简单粗暴。
今年年初我老家的卧室国产空调用十多年后罢工了,老爸趁着有国补换空调,我推荐了三菱电机。我和老婆回家开了几天感觉很非常好,安静且高效。国庆回家后发现我爸给他自己卧室也换上了同款空调。
PS 5 又吃灰了一整年,我还不考虑出售。总想着放假开起来玩通宵,但真到假期了,还是懒得开。
花了一个月时间通过了基金从业资格考试,除了琐碎的法规之外,也掌握了不少金融衍生品和金融工具(期权/期货/互换/逆回购/REITs/可转债/ABS...),尽管先前在投行的工作或多或少接触过,但要达到应付行业考试的程度还是得系统学习。这次学习机会重塑了我对投资的认知,几个重要的心得也在这里分享:
在学习之前,我总是在想市场的数学模型是什么,原来这正是马科维茨的现代投资组合理论。它用数学方法证明了不要把鸡蛋放在一个篮子里。它提醒我,真正成熟的决策并不是减少犯错,而是让系统在犯错时依旧可行。这种思路,在投资之外同样成立。
看了两场段永平访谈,总结起来还是那句话:买股票就是买公司。也经常睡前看猫笔刀(之前公司同事推荐的公众号),前几天还参与了一波场内白银 LOF 套利,虽然入场晚了点,但投机者慈善家是真金白银提供流动性。
摘录猫笔刀的一段话:
有人问我期指交割日是不是都会砸盘,我说不会,你只要去学习一下期指是什么样的金融产品,期指的运行方式,期指的交割规则,就知道它对 A 股没影响。但问题是这市场里 80% 以上的人不会学也不想学,他们更愿意接受不用动脑子的暴论,这样的人多到一定程度,市场就会自我实现。共识是有价值的,哪怕是错误的共识。
今年一直骑行上下班,每天骑五六十分钟,年底还被交警罚了一单。最近还感冒了一回,稍微吃点热食浑身就出汗了,没吃一粒药完全靠自愈。住的地方商圈发现了一家淮扬菜店,点了很多次他家外卖,还到店吃了几次。
多抓鱼上买了一些书,还淘到了一本绝版刊物《独唱团》。《南方周末》今年的新年献词写的像学生作文,也不知道现在报刊亭还有的卖么?或者,现在还有报刊亭吗?
我今年每一天都会刷 RSS,未来或者余生,我仍然会以它作为信息获取的主要途径;看到一些网站没有提供 RSS 源,我会给网站作者发邮件请求提供,再不济,我会基于 RSSHub 之类的工具自己生成对应的 Feed。
AI 应用爆发的年代,前文我提到人类获取信息的时间复杂度几乎达到了 O(1),我相信很多博主在博客上分享的也都不会再是纯知识类文章了,因为除了自我加深印象之外,没有意义。我也不会再写这样的内容,因为我的博客也不是维基。未来可能会分享自己对一些事物的理解,或者,单纯的流水账式吐槽。
大概就这些吧。
2024-04-04 08:00:00
Flyway 是一款开源的,基于 Java 实现的数据库内容变更控制工具。它提供了 CLI、Java API、Maven/Gradle Plugin 等多种方式方便开发人员将数据库的表结构改动、内容变动以可追溯的代码形式进行管理和部署。Flyway 支持包括大多数主流的关系型数据库:MySQL、SQL Server、Oracle Database、PostgreSQL、SQLite、TiDB、MariaDB 等,对于 MongoDB 的支持尚在预览阶段(更推荐 Mongock),同类竞品有 Liquibase,但 Liquibase 的使用相比 Flyway 更复杂,有额外的概念作为学习成本。
遵循 SpringBoot 广为人知的约定大于配置,Spring 官方提供了 Flyway 的自动配置实现。
@AutoConfiguration(after = { DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class,HibernateJpaAutoConfiguration.class })
@ConditionalOnClass(Flyway.class)
@Conditional(FlywayDataSourceCondition.class)
@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true)
@Import(DatabaseInitializationDependencyConfigurer.class)
@ImportRuntimeHints(FlywayAutoConfigurationRuntimeHints.class)
public class FlywayAutoConfiguration { /*...*/ }
全限定类名 org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration 会在检测到以下几种状态下实现自动配置:
Flyway 类 (即 org.flywaydb.core.Flyway)FlywayDataSourceCondition 类中的 Bean 或 Properties 条件,其实就是 DataSource 和数据库 URL 等连接配置存在spring.flyway.enabled=true 属性配置对于 MySQL 而言,除了必须的 JDBC 依赖之外,需要引入 Flyway 自身依赖,SpringBoot 已经在 pom 中声明过版本号,因此此处无需额外定义版本字段:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
具体使用有两种方式:
Flyway 会基于默认配置的文件夹路径 classpath:db/migration 发现版本变更文件或实现类。
V{VERSION}__{DESCRIPTION}.sql
V{VERSION}__{DESCRIPTION}.java
VERSION 可使用包含小数点、下划线的字符串版本,DESCRIPTION 则是简单的描述文本。中间的分割符是两个下划线。可通过 spring.flyway 配置自定义前后缀。
V{VERSION}__{DESCRIPTION}.sql 文件需要存放在 src/main/resources/db/migration 文件夹下; Java 类则需要定义包名 db.migration 并且继承父类 BaseJavaMigration,重写如下方法:
void migrate(Context context) throws Exception;
按照官方的写法,我们就可以在 migrate() 方法中去用 Java 代码实现数据库内容版本变动了,但是这里有一个缺点,那就是我们在 Spring 环境下无法针对 V{VERSION}__{DESCRIPTION}.java 进行依赖注入,只能尝试通过原生 JDBC 的方式、静态方法等去写比较朴素的 SQL 实现,而不能充分成分利用现有的 DAO 接口来做业务数据的更新。因此需要自定义配置 Flyway。
package me.lawrenceli.migration.config;
import jakarta.annotation.PostConstruct;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.migration.JavaMigration;
import org.flywaydb.core.api.output.MigrateResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class FlywayConfig {
// 自定义迁移历史表名
private static final String SCHEMA_HISTORY_TABLE = "schema_changes";
@Autowired
private DataSource dataSource;
@Autowired
private ApplicationContext applicationContext;
@PostConstruct
public void migrate() {
log.info("Flyway, 启动!");
// 通过 Spring 容器获取所有迁移实现类
// 这样一来,所有实现类就不再需要定义在 package `db.migration` 下,可以放在任何支持 Bean 扫描的位置。
JavaMigration[] migrationBeans = applicationContext
.getBeansOfType(JavaMigration.class)
.values()
.toArray(new JavaMigration[0]);
Flyway flyway = Flyway.configure()
.dataSource(dataSource) // 通过原本的 DataSource Bean 实现无需配置 flyway 自身的 JDBC URL
.locations("db/migration") // 默认迁移脚本路径
.table(SCHEMA_HISTORY_TABLE) // 默认迁移历史表为 `flyway_schema_history`
.baselineOnMigrate(true) // 默认 false, 对以存在的数据库做首次迁移必须设置开启
.baselineVersion("0") // 默认 "1"
.executeInTransaction(true) // 将迁移作为事务,你懂的
.installedBy(applicationContext.getId()) // 将微服务名作为迁移执行者
.javaMigrations(migrationBeans) // 注册迁移类
.load();
MigrateResult migrate = flyway.migrate(); // 执行迁移,依次调用子类实现
log.info("Flyway 迁移了 {} 版. {}", migrate.migrationsExecuted, migrate.success);
}
}
由于 Flyway 的配置基于这种手动配置,因此需要在 SpringBoot 启动类上排除原有的自动配置类,以防止自动配置存在加载冲突。
@SpringBootApplication(exclude = FlywayAutoConfiguration.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
最后,定义一个 Component Bean 去实现 BaseJavaMigration:
package me.lawrenceli.balabala.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class V2__QueryExample extends BaseJavaMigration {
@Autowired
private MyMapper myMapper; // Bean of DAO
@Override
public void migrate(Context context) throws Exception {
Data data = myMapper.selectById(2024L);
// ... other CRUD codes with Java
}
}
这样,所有的迁移类都可以方便地使用依赖注入来愉快地做 CRUD 了。经过实践,Flyway 会在数据库连接配置后、HTTP 服务暴露(也就是 Servlet 容器监听端口)前同步地执行完所有迁移,因此无需担心执行时机影响线上服务。
2023-12-03 08:00:00
最近看到很多博主在 App Defaults 中分享了他们的默认应用程序。以下是我自己的:
[email protected]。2023-06-11 08:00:00
Cloudflare 是一家在业内比较知名的 CDN 服务商,提供包含 DNS 解析、WAF 防火墙、CDN 加速、DDoS 防护,后续推出了一系列比较方便开发人员的许多功能:Cloudflare Workers、KV、Zero Trust Tunnel、WARP... 一切都是为了提供一个安全、快速的互联网环境。如果说 Vercel 给前端开发人员提供了基础设施,那 Cloudflare 则为数千万网站后端流量提供了基础设施。
今年年初,Cloudflare 缓解了破纪录的 7100 万个请求/秒的 DDoS 攻击。
三年前,我把我的博客从 WordPress 迁移到了 Vercel,老用户们或许都记得,那时候的域名可还都是 *.now.sh。初次使用 Vercel 的感受可以说是如获至宝,在今天看来可能显得很幼稚了——如果不是自己知道如何从 0 到 1 申请域名去部署一个全栈的 Web 项目的话,很难理解 Vercel 这种平台背后做了哪些复杂工作。这是我所理解的美国公司的一贯作法 —— 他们总是把庞大、精密、复杂的技术或基础设施掩盖在简约、优雅的产品外观之下。而我每次都会保持警惕和观察力,如果换我做,我怎么来实现?后来我便学习起了 Kubernetes。
说回 Cloudflare。从我第一次买域名(2015 年)一直到现在,我全部把解析权安排在 Cloudflare 上。归因于 Vercel 的一次网络问题,国内的网络受某种不可抗力在 2021 年的时候突然无法访问 Vercel 的部分域名了,尽管我的博客每天仅有为数不多的访客,但作为一个以中文为主的博客,还是有必要保持国内网络访问的畅通。根据官方提供的新的 CNAME 值,我在 Cloudflare 上更换了解析记录,也算顺利解决。也就在当时,我才注意到 DNS Records 控制台之前一个一直忽视的选项:
我好奇地把它启用了,即从灰色 DNS only 换成了这个橙色的 Proxied,那时我还没意识到,其实 Cloudflare 从那刻起已经完全接管了我的网站的全部流量并进行任播 (Anycast);换句话说,我在现有的 Vercel CDN 之上,又套了一层 Cloudflare CDN。是的,这迎来了两个问题:
不可能没人像我这样做吧?事实上,Vercel 并不推荐在其基础上使用另一层 CDN。后续我也依次找寻到了解决方案:对于 CNAME 来说,Vercel 会定时访问网站跟路径下的 .well-known 路径下的资源来识别包括 CNAME、HTTPS 证书这类配置验证网站控制权信息,因此我们可以直接在 Cloudflare 的 WAF 中,把这类路径作为白名单让 WAF 跳过其他安全规则直接放行。对于客户端 IP,可以参考 Available Managed Transforms,将一些客户端原始信息置于请求头中。缓存时间方面,还是要熟悉 MDN 上的一些标准 HTTP 协商协议,细粒度地对不同资源设置不同的 TTL,尽可能发挥 Cloudflare CDN 和浏览器自身缓存的优势 - 一个博客而已,是不是有点大炮打蚊子了?
Cloudflare 的整体防御从 L3 到 L7,遍布了所有能覆盖的防御范围。一个请求进入 Cloudflare 所代理的网站流量会经历顺序由上到下:
| Traffic Sequence in Cloudflare |
|---|
| DDoS |
| URL Rewrites |
| Page Rules |
| Origin Rules |
| Cache Rules |
| Configuration Rules |
| Redirect Rules |
| IP Access Rules |
| Bots |
| WAF |
| Header Modification |
| Access |
| Workers |
这些流量经过内部的层层筛选,以及我们自己定义的一些 Rule,最终反代到源站。因此,在决定使用任何 CDN 产品的时候,有必要将服务端源站 IP 妥善隐藏,尽可能不暴露任何历史解析值,否则一切防御都是徒劳。如果源站 IP 已经暴露,只能及时更换新的地址。在新的规则录入好后,Cloudflare 的全球网络会立刻应用规则并实时生效,这里或多或少要归功于大佬 agentzh 章亦春 开源的高性能网关 OpenResty。
我们可以将一个个「函数」部署在公有云的「边缘计算节点」之上,并暴露 Socket 给这些节点上的函数,来实现无需忽略底层服务器,直接部署可随意伸缩的 HTTP 服务的能力。当然,这要求这些函数尽可能无状态。在没有任何请求,闲置一定时间时,这些函数进程会直接消失以腾出计算资源,直到下次事件驱动它们迅速重新启动并继续提供服务。这便是老生常谈的 Serverless。
初次了解 Serverless 也是非常惊讶。AWS Lambda 竟能将 function 如此商业化 (FaaS),Vercel 在此之上也做到了开箱即用。借助 Cloudflare 现有的数据中心,Cloudflare 也推出他们的 Serverless 解决方案 - Cloudflare Workers。不同的是,Cloudflare Workers 相比原始的 Vercel Serverless Function 而言能够做 Server Sent Event、WebSocket 这类支持长连接的请求。尽管后续 Vercel Edge Function 也能实现,但是它能支持的 Node.js Module 实在太少了。(作者注:后来我才了解到 Vercel Edge Function 其实构建于 Cloudflare Workers 之上)
前不久,Cloudflare 开源了 Workers 运行时 workerd。
Cloudflare Workers 有许多应用场景。比如实现一个简单的短 URL 重定向服务、GitHub Proxy、以及一大堆各自实现的 ChatGPT API Proxy...方便了太多国内用户。
Node.js 作者 Ryan Dahl 这几年给 JavaScript 写的另一个全新运行时 Deno 也有类似的 Serverless 服务,体验也很友好,同样支持 Web Standard API。
为了实现 Serverless 的更多数据持久化功能,他们也各自推出了自家的 KV 存储实现服务,或者说是 Serverless 数据库。
再来谈谈技术方面的一些进展。很多读者都知道 Server Name Indication(服务器名称指示,SNI)的存在,它是 TLS/SSL 协议在最初的 Client Hello 阶段由客户端发往服务端的一个字段,内容是网站的主机名或域名。引用 Cloudflare 的形象解释:
SNI 有点像邮寄包裹到公寓楼而不是独栋房子。将邮件邮寄到某人的独栋房子时,仅街道地址就足以将包裹发送给收件人。但是,当包裹进入公寓楼时,除了街道地址外,还需要公寓号码。否则,包裹可能无法送达收件人或根本无法交付。许多 Web 服务器更像是公寓大楼而不是独栋房子:它们承载多个域名,因此仅 IP 地址不足以指示用户尝试访问哪个域名.....当多个网站托管在一台服务器上并共享一个 IP 地址,并且每个网站都有自己的 SSL 证书,在客户端设备尝试安全地连接到其中一个网站时,服务器可能不知道显示哪个 SSL 证书。这是因为 SSL/TLS 握手发生在客户端设备通过 HTTP 指示连接到某个网站之前。
有点类似于 HTTP 协议中的 Host 请求头(如果在同一台服务器上用 Nginx 配置过多个虚拟主机应该都熟悉),但是 SNI 是作用在 L4,而且在 TCP 握手前完成。起初它并不是 TLS 协议的一部分,最早在 2003 年作为扩展字段增加到 TLS 协议中 (RFC 6066)。现代浏览器等客户端都早已支持这个字段。我们会发现一个细节问题,对基于同一 CDN 的网站的 HTTPS 请求,我们传入的 TLS SNI 和 HTTP Header Host 会有不一致的情况,在不严格校验 SNI 的情况下,这类请求有可能被路由到 Host 所定义的主机上,本质也就无视了 SNI,因此对于某些防火墙来说,由于它们能通过 SNI 来侦测到用户所请求的 HTTPS 站点,它们无法得到后续 TLS 握手后的 HTTP 报文内容,在客户端更换了 Header Host 后,实际返回的 HTTP 报文内容其实已被调包 —— 这种攻击方式,或者说叫伪装方式被称为域前置(Domain Fronting)技术。Cloudflare、CloudFront 都会校验二者的一致性返回 403,但依然有部分 CDN 对这一做法采取保留,比如 Fastly。
我们可以用 WireShack 抓包获取到 SNI 字段。应用这个过滤条件 ssl.handshake.extensions_server_name,尝试抓包发送一次 TLS 请求
openssl s_client -connect lawrenceli.me:443 -servername lawrenceli.me -state -debug < /dev/null

可以从结果看出,SNI 确实使用了明文进行传输,这就导致了前文提到的一个问题 - 就算经过 TLS/HTTPS 加密的流量,仍然明文地暴露了我们在访问的域名。「这又如何?DNS 不也暴露了嘛?」好问题 - DoH 解决了 DNS 请求的明文风险 (RFC 8484)。因此,实际上 TLS 目前唯一在数据上有泄密风险的就只有这个字段了。Cloudflare 先后搬出了两个解决方案:ESNI 以及 ECH。
我们可以使用 Chrome 的开关 chrome://flags/#encrypted-client-hello 来开启浏览器 ECH 的客户端支持。通过 Chrome DevTool 的 Security Tab 能够查看 HTTPS 流量的安全性信息。我们可以通过这个链接来测试客户端对这个方案的支持情况,当然,这些需要服务端做相应的配置才能完全启用。话题就此结束,我不能再细说了。
Updated:2023 年 9 月底,Cloudflare 宣布向所有基于 TLS 1.3 的代理站点启用 ECH,目前默认全部启用且改选项不可关闭。
利用 Client Hello 来做安全保护的另一个实践是 TLS 客户端指纹: JA3 & JA3S。这一设计灵感来源于信息安全专家 Lee Brotherston 的研究 TLS fingerprinting。
具体的工程实践可以参考 Salesforce 开源的 JA3.
简而言之,TLS 握手过程中客户端发送的字节数组,也就是 Client Hello 阶段的一些字段和扩展名,通过固定方式拼接,基于摘要 MD5 来生成一个唯一的字符串,称为 JA3 指纹。不同的浏览器或 TLS 客户端有不同指纹。在大量的数据采样中,Cloudflare 就能够基于此数据 (JA3 & JA3S,后者包含了 Server Hello 阶段的服务端指纹) 统计出哪些请求来自于僵尸网络、机器人爬虫、Python 库还是正常用户的浏览器、或者手机访问。这也就解释了很多同学写爬虫时,利用 HTTP 协议更换 User-Agent 这一请求头无效的情况,因为 Cloudflare 的防御处在更底层的 L4 TLS 阶段。ChatGPT 的 Web 端也部署了 Cloudflare 的 TLS JA3 指纹鉴定 WAF (仅限 Enterprise 账户);GitHub 上我也找到了相关的代码实现通过更换 TLS Client 的方式来绕过这一防御。对于多数人来说,这已经有很大的防爬门槛了;而且 Cloudflare 可以随时更换 WAF 策略让旧的指纹失效。
JA3 由来自 salesforce 的三位工程师共同实现:John Althouse, Jeff Atkinson & Josh Atkins。看到这里,想必你也知道为什么 JA3 叫 JA3 了。
和 Vercel,Netlify 如出一辙,Cloudflare 采用「免费试用,付费增值」的商业模式。Cloudflare CEO Matthew Prince 曾在 StackOverflow 上回答过这个问题:「How can Cloudflare offer a free CDN with unlimited bandwidth?」:
2019 年,Cloudflare 在纽交所上市,股票代码:NET。发行价 US $15,目前 US $63,上涨了 320%。画外音:现在买它还来得及吗?
在中国目前和京东云合作,仅限企业用户。500 强企业中目前有三分之一使用 Cloudflare,还有很多上升空间。OpenAI 的 ChatGPT 上线后,Cloudflare 获得了大量曝光,防御了大量滥用用户和潜在威胁请求。
Cloudflare 因坚持网络中立原则受到了一些批评。
比较典型的一件事是 Cloudflare 因舆论和法律的压力终止对 8chan 的服务。Cloudflare 声称自己是一家私营公司,并且 Cloudflare 半数营收来自于美国之外的地区,可以不受美国宪法第一修正案的约束,其服务的客户对象是整个互联网市场。由于业务量大,有些包含恐怖主义、仇恨言论的网站也免不了会使用其服务。这也是大多数大型互联网公司所面临的问题。和快播王欣事件类似,他们都不愿扮演内容仲裁者。互联网诞生至今,法律的步伐总是跟不上技术的发展。
OpenAI 的 ChatGPT 对 Cloudflare 作了一次很好的展示,我向读者推荐 Cloudflare。一方面是因为它一直提供永久的个人免费服务,另一方面是它的易用性以及全球视野。我也用过国内某套路厂商的 WAF 产品,界面纷繁错乱,一看账单都不知道为什么收费,套路太深,价格高昂(可能怪我太穷)。
2023-01-21 08:00:00
过年回老家看电视,运营商送的网络电视盒子主屏幕花花绿绿,我一个程序员都费了好久才找到地方卫视的直播频道。
索性去电商平台搜搜看有没有更好的硬件、翻看各种测评文章和视频。清晰了需求定位后,我果断找了一家有现货的店铺下单了一款美版 Apple TV 2022 (4K)。

一直担心年前收不到货,没想到快递很敬业地发过来了。
由于购买之前就已经熟悉了大部分使用细节,所以安装、使用的时候毫不费力;像是把玩过很久的玩具一样自然流畅。
用美区 Apple ID 购买了很多付费应用,主要都是一些国内独立开发者的作品。
很难想象不少开发者会为国内极其小众的平台开发上架了如此小而美的 tvOS App。
搜集一些电视直播源,我就反常地看起了 CCTV。比期待的画质高出不少。遥控器的金属质感像第一次摸到棱角分明的 iPhone 5S 一样爱不释手!还用它和我爸玩了一局桌球游戏。
种种体验让我想起知乎上一个回答:
长这么大,听过最清晰的《义勇军进行曲》是在 Apple Music。