2025-01-23 14:19:09
在 上一篇文章中,我介绍了swiss map
跟 Dolthub 实现的一个Go语言版本。对于 swiss map
不太熟悉的读者,可以先去看看那篇文章。
在即将正式发布的Go1.24中swiss map
将作为现有map
的替代者正式进入gosrc。它完全兼容现有API,并且在部分benchmark
场景下带来了超过50%的性能提升。到目前为止,swiss map
是我对于Go1.24最期待的功能了。可是它真的有期待中的那么好嘛?本文将从兼容性、扩展哈希(Extendible Hashing的实现与优势,以及遗留问题三个方面,深入剖析这一新设计的核心逻辑。
Go 的swiss map
设计目标之一是与旧版 map
兼容。通过条件编译标签和类型转换,实现在新旧版本间的无缝切换。例如,在 export_swiss_test.go
中,newTestMapType
函数直接通过类型转换将旧版 map
的元数据转换为 swiss map
的类型结构:
|
|
这种设计允许现有代码无需修改即可通过实验性标志启用 swiss map
,同时保留了旧版哈希表的内存布局兼容性。当前gotip(go1.24-3f4164f5) 中GOEXPERIMENT=swissmap 编译选项已经默认打开,也就是默认使用的是swiss map
。
如果您还是想用 原来的map可以使用 GOEXPERIMENT=noswissmap
与其他其他几个社区实现,除了在兼容性方面的改进之外,swiss map
的核心创新之一是采用了Extendible Hashing
(扩展哈希),以支持高效的增量扩容。传统哈希表在扩容时需要全量迁移数据,而 Extendible Hashing
通过多级目录和表拆分,将扩容开销分摊到多次操作中。
在 map.go
的 Map
结构体中,globalDepth
和 directory
字段是关键:
|
|
dir大小为 1 << globalDepth
,每个dir项指向一个 table
。当某个 table
的负载过高时,会触发拆分(Split),而非全局扩容。
拆分(Split)是 swiss map
在单个表容量达到 maxTableCapacity
(默认为 1024)时触发的动态扩容机制。其核心目标是将一个表的负载分摊到两个新表中,避免全局扩容的高延迟。以下是拆分的关键步骤与地址变化:
拆分时,原表(假设为 table A
)会创建两个子表 table Left
和 table Right
。它们的 localDepth
(本地深度)比原表大 1,表示其哈希掩码多使用了一个高位比特。
|
|
left
和 right
是新分配的内存对象,其 groups.data
指向新分配的连续内存块(通过 newarray
函数)。
拆分时,根据哈希值的高位比特(由 localDepth
决定)将原表的数据分配到左表或右表。例如,若 localDepth
为 2,则使用哈希值的第 2 个高位比特(从最高位开始计数)作为分配依据。
|
|
localDepthMask
根据 localDepth
生成一个掩码,例如:
localDepth=1
→ 0x80000000
(32 位系统)或 0x8000000000000000
(64 位系统)。localDepth
个高位比特。拆分完成后,需要更新全局目录(Map.directory
),使原表的索引范围指向新的子表。如果原表的 localDepth
等于全局的 globalDepth
,则目录需要扩展(翻倍)。
假设原表 table A
的 localDepth=1
,globalDepth=1
,目录大小为 2
(1 << 1
)。拆分后:
globalDepth
增加到 2,目录大小变为 4
(1 << 2
)。0-1
)被替换为指向 left
和 right
表。
|
|
table A
的 dirPtr
指向的目录项被更新为新表的地址。[A, A]
变为 [Left, Right]
(扩展后目录为 [Left, Right, Left, Right]
)。globalDepth=1
,目录大小为 2,指向同一个表 A
。
|
|
Left
(localDepth=2
)和 Right
(localDepth=2
)。globalDepth
增加到 2,目录大小变为 4。
|
|
directory[0]
和 directory[1]
的指针从 A
变为 Left
。directory[2]
和 directory[3]
的指针从 A
变为 Right
。假设原表 A
的哈希键分布如下:
00
或 01
→ 分配到 Left
。10
或 11
→ 分配到 Right
。通过掩码 localDepthMask(2)
(例如 0x40000000
),提取哈希值的第 2 个高位比特,决定数据归属。
groups.data
是连续内存块,利于缓存优化。此外,swiss map
针对一些特定场景也有优化,比如针对于少量元素(<=8)就直接使用一个group来存储数据,尽量降低 swiss map
在少量数据的时候的性能劣势。
尽管 swiss map
在设计与性能上迈出了一大步,但仍存在以下待优化点:
当前实现通过 writing
标志检测并发写入,但缺乏细粒度锁:
|
|
这在多线程高并发场景下可能导致竞争条件。官方文档提到未来可能引入更复杂的同步机制,但目前仍需依赖外部锁。
swiss map
的 group
结构(8 控制字节 + 8 键值槽)可能导致内存对齐浪费,尤其是键值类型较小时。例如,若键为 int32
、值为 int8
,每个槽位将浪费 3 字节。
Iter
的实现(table.go
)需处理目录扩展和表拆分,逻辑复杂:
|
|
在频繁扩容的场景下,迭代器的性能可能受到影响。
此外,在最新的代码中,遗留了很多TODO
我甚至都不知道如何总结这些TODO, 目前距离 Go 1.24 正式发布还有不到一个月的时间,不知道这些TODO会不会全部解决。
在社区中关于swiss map
的讨论也还有很多,主要集中在性能优化方面,可以想象未来 还会出现很多变动。
一切都要等到2月份正式发布。
完整的测试代码:https://github.com/hxzhouh/gomapbench
go version devel go1.24-3f4164f5 Mon Jan 20 09:25:11 2025 -0800 darwin/arm64
平均性能提升 28% 大约,在某些场景下,甚至高达 50%。不愧是Go1.24 最期待的功能。
但是,这是一份不严谨的性能测试,测试机器也仅限于我自己的笔记本电脑,有一些报告显示swiss map
在某些地方的性能甚至出现了下降。
https://x.com/valyala/status/1879988053076504761
https://x.com/zigo_101/status/1882311256541102178
最终swiss map
如何,还是要看后续的演化。
Go 1.24 的 swiss map
通过兼容性设计、Extendible Hashing 和优化的探测序列,显著提升了哈希表在高负载场景下的性能。然而,其并发模型和内存效率仍有改进空间。对于开发者而言,这一新特性值得在性能敏感的场景中尝试,但也需关注其当前限制。
https://tonybai.com/2024/11/14/go-map-use-swiss-table/
https://github.com/golang/go/issues/54766
https://pub.huizhou92.com/swisstable-a-high-performance-hash-table-implementation-3e13bfe8c79b
https://www.geeksforgeeks.org/extendible-hashing-dynamic-approach-to-dbms/
2025-01-16 14:44:41
对冲请求模式出现在论文The Tail At Scale中,是Google 解决微服务长尾效应的一个办法.也是gRPC中两种重试模式之一。
对冲请求客户端将同一个请求发送到不同的节点,一旦收到第一个结果,客户端就会取消剩余的未处理请求。
这种模式主要作用是为了实现可以预测的延迟。假设我们的服务的一个调用链路是20个节点,每个节点的P99是1s,从概率上讲,一定有 18.2% 的请求时间大于1s。
通过对冲模式,我们每次都是从最快的节点那里得到结果,所以不会存在不可预测的长尾延迟(服务故障不在考虑范围之内) 。
在Golang中,我们可以使用context很方便的实现对冲请求,比如在下面的例子中:对于同一个后端服务,我们发起五次请求,只取最先返回的那次。
|
|
完整的代码 请访问:https://go.dev/play/p/fY9Lj_M7ZYE
这样做的好处就是,我们可以规避服务的长尾延迟,使服务的之间的延迟控制在可控的范围内。不过直接这么实现会造成额外的多倍负载。需要仔细设计。
出现长尾延迟的原因有很多,比如
STW
会放大长尾延迟有什么办法可以避免对冲请求模式造成的 请求放大嘛?Go High-Performance Programming EP7: Use singleflight To Merge The Same Request 中详细介绍了如何使用 SingleFlight
来合并相同的请求。这个场景下面,使用SingleFlight
能够一定程度的缓解重复请求。
还有一种做法是只发送一个请求, 到P95的时候,如果还没有收到返回,那么就立即向第二个节点发送请求。这样做的好处就是将重复请求缩小到5%。并且大大缩短了长尾请求。
在这篇论文中,还有一些方法可以用来解决,长尾请求
你还有其他处理长尾请求的好方法吗?
2025-01-14 21:18:02
假设有这么一个场景: 假设100w个Uber司机,司机客户端每隔10分钟上报一次数据,如果十分钟没有上报数据,服务端会将这个司机设置为离线状态,不给他派单。
我们应该如何实现这个功能?
通常的情况下,我们会使用redis的zset等第三方组件来实现这个功能,但是如果不使用第三方组件呢?只使用内存呢?大概有这么几种方案:
|
|
缺点:这种方式效率不高,因为需要定期轮询整个Map,时间复杂度较高。
另一种方案是为每个司机分配一个Goroutine来管理其心跳超时。
|
|
缺点:虽然不需要轮询,但每个Goroutine都有栈空间,当司机数量非常多时,内存消耗会很大。此外,Timer的创建和删除时间复杂度为O(log n),效率有待提升。
前面铺垫了这么久,终于轮到我们的主角了,时间轮。
时间轮是一个比较有趣的算法,他最早刊登在George Varghese和Tony Lauck的论文里。 时间轮算法的核心逻辑是:
所以我们可以使用时间轮算法来实现功能。我们可以将interval设置成1s, 时间轮的slots为600,司机上报数据的时候,将记录插入到position-1的slot里面。
一个简单的Demo:
https://gist.github.com/hxzhouh/5e2cedc633bae0a7cf27d9f5d47bef01
所以时间轮特别适合以下场景的任务:
将所有任务的换算为多少秒或毫秒(Interval)后到期,维护一个最大过期值(Interval)长度的数组。比如有10个任务,分别是1s,3s,100s 后到期,就建一个100长度的数组,数组的index就是每个任务的过期值(Interval),当前时间作为第一个元素,那么第二个元素就是1s 后到期的任务,第三个是2s 后到期的任务,依次类推。当前时间随着时钟的前进(tick),逐步发现过期的任务。
简单时间轮虽然很完美,所有的操作时间复杂度都是O(1),但是当任务最大到期时间值非常大时,比如100w,构建这样一个数组是非常耗费内存的。可以改进一下,仍然使用时间轮,但是是用hash的方式将所有任务放到一定大小的数组内。 这个数组长度可以想象为时间轮的格子数量,轮盘大小(W)。
hash的数值仍然是每个任务的到期值(Interval),最简单的是轮盘大小(W)取值为2的幂次方,Interval哈希W后取余,余数作为轮盘数组的index,数组每个元素可能会有多个任务,把这些任务按照过期的绝对时间排序,这样就形成了一个链表,或者叫做时间轮上的一个桶。
但是Hash有序时间轮 还是有一个问题:
因为只使用了一个时间轮,处理每一格的定时任务列表的时间复杂度是 O(n),如果定时任务数量很大,分摊到每一格的定时任务列表就会很长,这样的处理性能显然是让人无法接受的。
层级时间轮通过使用多个时间轮,并且对每个时间轮采用不同的 u,可以有效地解决简单时间轮及其变体实现的问题。
参考 Kafka 的 Purgatory 中的层级时间轮实现:
总结一下几种算法的性能。
算法 | 添加任务复杂度 | 删除任务复杂度 | 内存开销 | 适用场景 |
---|---|---|---|---|
Single Timer | O(1)O(1) | O(1)O(1) | 低 | 适用于任务数量少、精度要求高的场景。 |
Multi Timer | O(logn)O(\log n) | O(logn)O(\log n) | 中 | 适用于任务数量中等、任务间相互独立的场景,但内存开销较高。 |
Simple Timing Wheel | O(1)O(1) | O(n)O(n) | 高(大数组) | 任务分布均匀、到期时间精度要求较低的场景。 |
Hash Timing Wheel | O(1)O(1) | O(n)O(n) | 中 | 任务数量较多、分布不均匀但对精度容忍较高的场景。 |
Hierarchical Timing Wheel | O(1)O(1) | O(logn)O(\log n) | 低到中 | 适用于大规模任务、层级管理复杂任务、需要较长生命周期的任务调度场景(如 Kafka 和 Netty)。 |
2025-01-11 19:35:01
错误处理一直是编程中的重要组成部分, Go语言因为它独特的错误处理模式饱受争议,任何一篇写如何讨厌Go语言的博客中,一定会把“繁琐的错误处理”放在靠前的位置。这个问题在 Go 社区引发了大量讨论,探讨如何在保持清晰性和可维护性的同时减少模板代码。
ianlancetaylor提出了一个新的提案#71203 ,在 Go 中引入用于错误处理的操作符?
。用来简化Go的错误处理。后续Go的错误处理可能会变成这个样子:
|
|
在本例中,两种写法的结果是相等的:如果 someFunction()
返回错误,就返回。
这个proposal的 核心内容就是这样了, 主要目的是减少templ代码,同时保持 Go 的显式和简洁理念。?
是一个语法糖,在返回多个值的函数调用(例如 (T, error)
)之后使用时,它会自动检查最后一个值是否为非零(表示错误)。编译器将为 这种写法生成跟以前一样的代码,保证兼容性。
在正式提案中,ianlancetaylor详细阐述了?
的语法规则:
?
只能出现在赋值语句或表达式语句的末尾,并且表达式必须要有返回值?
“吸收”的是表达式的最后一个值(通常是err)?
“吸收”的是右侧表达式的最后一个值(通常是err),这样右侧值的数量会比左侧变量的数量多一个。?
后面可以跟一个代码块。如果没有代码块,当qvalue不为nil时,函数会立即返回,并将qvalue赋给最后一个返回值。如果?
后面有代码块,当qvalue不为nil时,代码块会被执行。在代码块中,会隐式声明一个名为err的变量,其值和类型与qvalue相同。基本的使用场景可能是这样子的:
|
|
这个 proposal 最重要(也是唯一的好处)好处是减少 Go 程序中的重复代码数量, 根据proposal 中的描述.
reduces the error handling boilerplate from 9 tokens to 5, 24 non-whitespace characters to 12, and 3 boilerplate lines to 2.
跟以前的错误处理提案try 等不同的是, ?
不会引入隐藏的控制流, ?
的存在明确地指示了错误处理的逻辑。
最大的缺点就是所有的Go图书、资料需要更新,并且对于新人来说,可能需要理解这个概念,因为它跟其他语言的实现都不太一样。并且这个改动,会涉及很多代码,包括go src,所以Go Core Team
的压力也很大,因为机会只有一次。
?
后面的代码块会隐式声明一个err变量,这可能会导致变量shadowing的问题。 proposal 中提到了一个例子,
|
|
In this example, the assignment
err = nil
has to change theerr
variable that exists outside of thefor
loop. Using the?
operator would introduce a newerr
variable shadowing the outer one.
In this example, using the?
operator would cause a compiler error because the assignmenterr = nil
would set a variable that is never used.
在这个例子中,赋值
err = nil
必须改变存在于for
循环之外的err
变量。如果使用?
操作符,就会引入一个新的err
变量,遮蔽外部变量。
在本例中,使用?
操作符还会导致编译器错误,因为赋值err = nil
会设置一个从未使用过的变量。
|
|
在这个例子中,这两个函数都合法,只有G2的换行符有差异,但它们的行为却完全不同。 这个差异可不能通过fmt等方式找补回来。
尽管Go的错误处理机制经常受到批评,但它仍然是可用的。因此,社区需要权衡是否真的需要进行改变。在proposal中,ianlancetaylor反复提到: “Perhaps no change is better than this change. Perhaps no change is better than any change"。这也一定程度上反映出Go Core Team
在错误处理改进方面其实并不那么坚定,感觉更多是迫于Go社区的舆论和压力。
泛型: 别Q我
新的proposal可以看出Go Core Team
还是在听社区的声音。?
操作符提案为Go语言的错误处理机制提供了一种新的思路。该提案通过引入简洁的语法,可以显著减少错误处理的代码量,并使代码的主流程更加清晰。尽管现在还存在一些分歧,但是总算有人在推动不是?
2025-01-09 18:42:34
原文链接:Go is a Well-Designed Language, Actually
哈哈,没有泛型 —— 这是一句古老的程序员谚语。
从诸多方面来看,2009 年为我未来的职业生涯埋下了伏笔。那时我 13 岁,刚在一场足球赛里打进了人生中的第一粒进球。那是一次精彩的二过一配合,最后我一记大力抽射,球直入球门左上角。可惜的是,那天球探不知去向。当我还憧憬着踏入温布利球场的那一刻,Go 语言诞生了。
Go 语言很快就吸引了大批拥趸。大家钟情于它的简洁性,还有它对 Web 服务的优化,以及像 gofmt
这类实用工具。不过,凡事皆有两面,Go 语言也不例外。有人嫌弃它太过简单,抱怨它只能用来捣鼓蹩脚的 REST API,还吐槽那些过于 “热情” 的工具。
在过去的 15 年里,人们写下了大量对 Go 语言的批评,甚至是愤怒的吐槽。其中让我格外留意的,是有人认为 Go 语言设计得很糟糕。这一观点在两篇文章里体现得尤为明显 —— 《我想摆脱 Golang 先生的狂野之旅》 和 《我们告诉自己继续使用 Go 的谎言》,均出自 fastthanlime 之手。后者更是直言:
所以他们没有。他们没有设计语言。它就这么 “冒出来” 了。
在我看来,设计就是达成目标的计划或规范。打个比方,BBC 新闻网站的目标是向用户通报全球发生的、与他们切身相关的大事。为实现这一目标,网站会撰写新闻报道,再依据事件发生地和重要程度进行排序。毕竟,一枚朝我飞来的核弹,可比一只挂在树上的猫要紧要得多。
所以,判断一个设计好不好,要看它能在多大程度上实现既定的设计目标。
Go 语言诞生于谷歌,Russ Cox、Rob Pike、Ken Thompson 等众多大咖都效力于谷歌。彼时,谷歌内部主要使用 Java 和 C++。Go 语言的设计者们觉得,这两门语言性能虽优,但用起来实在费劲。编译器慢吞吞的,工具还特别挑剔,而且它们的设计至少都是十年前的老黄历了。与此同时,云计算 —— 大量多核服务器协同作业,正变得日益普及。
于是,他们决定打造一门属于自己的语言,优先考虑让它能在大规模的计算任务以及人力协作方面游刃有余。Rob Pike 在 Go at Google 一文中解释道:
硬件规模庞大,软件亦是如此。软件动辄数百万行代码,服务器端大多用 C++ 编写,其余部分则大量采用 Java 和 Python。数千名工程师投身于代码编写工作。
在别的场合,Rob Pike 一如既往地以谦逊、含蓄的口吻谈及他所面向的那数千名工程师:
关键在于,我们的程序员是谷歌员工,而非科研人员。高深精妙的语言,他们可玩不转。
重要提示:要是你正在搞设计,千万要避免贬低、居高临下地对待你的设计受众。
尽管有这么一段引发争议的言论,不过我们还是能看出一个相当合理的设计目标:这门语言得让编写和维护大型并发服务器代码变得轻松容易,哪怕使用者是数千名技能水平参差不齐的开发人员。
咱们来瞧瞧人们对 Go 语言的一些怨言,再依据它的设计目标来评判一番。
Go 语言的文件系统 API 常常遭人诟病,原因是它太偏向 Unix 系统了。Windows 系统不像 Unix 那样有文件权限一说,所以 Go 语言只能返回一些形同虚设的权限。而且,Go 对路径的处理相当简单粗暴。操作系统有自己的路径分隔符,而路径在 Go 里就是 string
类型 —— 仅仅是一串字节,没有任何实质性的检查或限制(译者注:在 go 1.24 版本中使用 os.ROOT
会改善不少)。
其他语言在这方面就严谨得多。比如 在 Rust 里获取文件修改时间的方法,有可能返回 None
。Zig 语言里 文件的元数据会因操作系统而异。
不过从设计目标的角度来看,这倒也情有可原。Go 语言本就是为谷歌量身打造的,和大多数服务器一样,谷歌的服务器 清一色用的是 Linux。要是你设计一门主打服务器应用的语言,以 Unix 为核心来打造文件系统 API,不失为一个明智之举。
在 Go 语言里,和 Java 不同,函数和方法只有单一的定义(一旦指定了构建标签和目标)。与 C++ 迥异的是,运算符是在编译器里预先实现好的,无法重载。在 time
包里,要是想把 Duration
类型的值加到 Time
类型上,得用 Add
方法。要是你想增加两天,可不能像这样调用 Add(0 /*years*/, 0 /*months*/, 2 /*days*/)
,而得用 AddDate
方法。
在有些人眼里,这显得不够优雅,但它胜在简洁明了。在 Go 代码里看到函数调用,你心里清楚只需查看一处定义就行。要是瞅见一个运算符,你也明白它是针对内置类型的,干的肯定是靠谱的事儿,绝不会是 铸造 NFT 这种奇葩操作。
公允地讲,当下编程语言的潮流是追求简洁。也难怪程序员们都反感 Go 语言里那种 if err!= nil
的错误处理风格。
然而,这也是深思熟虑后的抉择:
虽说相比之下,Go 语言检查错误的写法更啰嗦,但这种显式设计让控制流程一目了然 —— 就是字面意义上的清晰。
清晰明了的控制流程让代码的可读性更强。虽说支持异常处理的语言写起代码来可能更快,但生成的代码没那么简洁,而且控制流程藏得很深。
Go 语言常常因避开异常处理这类特性而饱受批评,有人觉得这简直是开倒车。曾经有人质问设计者:“为啥你们对 20 世纪 70 年代以来有关类型系统的研究成果一概无视?”。类似的论调 在别处也屡见不鲜。
首先,Rob Pike 可瞧不上这种傲慢,也压根儿不 care:
Go 旨在解决谷歌在软件开发过程中遭遇的难题,这就使得这门语言虽说算不上开创性的科研语言,但用来搞大型软件项目,那绝对是把好手。
其次,将错误设计成明确的值,已然成为一种(再度)引领潮流的做法。Go、Rust 和 Zig 都选用了这种方式。Swift 语言即便支持异常,也要求你在函数签名里标明哪些函数可能会出错。
译者注:FFI,中文名叫语言交互接口 (Foreign Function Interface),指的是能在某种计算机语言里调用其他语言的接口。
Go 语言与其他语言的兼容性欠佳。要是你想调用 C 函数,比如使用 SQLite,那就得通过 CGO。要知道,CGO 可不是纯正的 Go,还存在性能损耗。由于 goroutine(拥有由 Go 运行时设定的专属堆栈)是执行单元,Go 就得按照 C 语言的期望来做一些操作以获取堆栈,这成本可不低。
Go 语言的 FFI 表现不佳,还因为它有自己的编译器、链接器和调试器。Go 生态系统里的好多东西都是定制化的。
不过,考虑到设计目标的话,这也说得通。服务器软件必须支持并发,所以采用了 goroutine。这必然会让调用 C 代码变得复杂些,但这种权衡利弊,至少适配 Go 用于服务器间通信而非进程间通信的并发系统。
这些决策也让 Go 语言在工具方面占尽优势。编译器是专为 Go 打造的,这意味着它能一门心思地快速编译 Go 代码。调试器能够理解 goroutine 以及 Go 的所有内置类型。
这就见仁见智了。就我个人而言,我挺喜欢它的。我经手过的 Go 代码,读起来、理解起来通常都不费劲。它没有那些花里胡哨的东西,逼着我一门心思写实在的代码,而不是构建些华而不实的抽象概念。我还成功地向一大帮刚从大学毕业的新人传授过 Go 语言。
但这并不意味着我对它的缺点视而不见。有一回,我跟一位客户通电话,他碰上一个错误,就因为没检查错误,我们费了好大劲才追踪到问题所在。要是开着 Linter
,这事儿本可轻松避免,可要是没开,那就麻烦了。Go 语言长久以来都不支持泛型,编写泛型数据结构的时候可费劲了。每次收到一份关于 Windows 系统的错误报告,我都得停下来琢磨琢磨,是不是 Go 语言让我产生了一种错误的安全感?
说到底,这些问题都是设计过程中有意权衡取舍的结果。你可以说不喜欢 Go 语言,或者它不适合某个应用场景,又或者它满足不了你的需求。甚至,你大可以直言讨厌它。但千万别断言它设计得糟糕。
2025-01-09 16:01:05
近期,Dennis Schubert发布了一则帖子,称 “diaspora*” 项目的网络基础设施因为访问流量过大而陷入了性能瓶颈。令人震惊的是,他发现70% 的请求来自 IT 巨头公司的 LLM(大语言模型)爬虫。这些爬虫无视 robots.txt
文件,贪婪地抓取网站的所有可用数据,甚至是一些无关紧要的内容。
Dennis 感到无比愤怒,因为 ChatGPT
和 Amazon
的爬虫竟然爬取了 Wiki 的全部编辑历史,每一页的每次编辑都被记录下来。他质问:
“他们到底要做什么?是想研究文本如何随时间变化吗?”
这种对数据的无底线掠夺,导致服务器负载极高,用户访问体验显著下降。Dennis 尝试了一些反制措施:
最终 Dennis 感慨,这种行为已经接近于对整个互联网的DDoS 攻击。
答案是:AI 数据饥荒。
随着大模型的普及,用于训练 AI 的高质量语料已经见底。正如 OpenAI 工程师 James Betker 所言:
“模型优劣的关键在于数据集的质量。它们正在以惊人的精度复刻数据集。”
为了在 AI 竞赛中领先,巨头们不惜一切代价获取更多数据。个人网站、自建 Wiki,这些原本属于小众的内容,正成为巨头们争相攫取的目标。
IT 巨头拥有顶尖的爬虫和反爬虫技术团队,能够在抓取与用户体验之间找到平衡。但对于个人网站和小型项目来说,这无疑是一场不对等的战争。
Dennis 提出了以下两种反制策略:
尽管这些方法可能有效,但实现起来成本不菲。
巨头公司的终极目标是什么?
是将用户牢牢锁定在他们的生态系统中。通过 AI 提供“最优内容”,用户无需访问其他网站,甚至看不到其他链接。一切内容直接呈现,广告作为附加品,而创作者只能沦为巨头的数据供应商。
这种趋势正在瓦解互联网的开放性。
无论你如何优化 SEO 或产出优质内容,巨头的 AI 会优先抓取并整合,用户永远不会直接访问你的网站。最终,个人创作者将失去流量与收入,整个互联网变成巨头的“金矿”。
IT 巨头正在用技术手段,掠夺数据,榨取价值,逐步摧毁互联网的多样性与开放性。对于个人网站而言,我们几乎无力抗争,而这场改变已经不可逆。