开发者需警惕的编程语言和开发工具陷阱

对开发者陷阱的总结。这些陷阱是容易被误解且导致 bug 的非直观事物。

陷阱总结

Java

  • == 比较对象引用。应使用 .equals 比较对象内容。
  • 忘记重写 equalshashcode。在地图键和集合中,默认会使用对象身份相等性。
  • 修改地图键对象(或集合元素对象)的内容会导致容器故障。
  • 返回 List<T> 的方法有时会返回可变的 ArrayList,有时会返回不可变的 Collections.emptyList()。尝试修改 Collections.emptyList() 会抛出 UnsupportedOperationException
  • 返回 Optional<T> 的方法可能返回 null(不推荐,但实际代码库中确实存在这种情况)。
  • finally 块中的返回值会吞噬 trycatch 块中抛出的任何异常。方法将返回 finally 块中的值。
  • 中断。某些库会忽略中断。中断可能导致包含 I/O 的类初始化失败。
  • 线程池默认不会记录通过 .submit() 提交的任务的异常。您只能从 .submit() 返回的未来对象中获取异常。不要丢弃未来对象。如果抛出异常,scheduleAtFixedRate 任务会静默停止。
  • 以 0 开头的字面量数字将被视为八进制数。(0123 是 83)
  • 在调试时,调试器会调用 .toString() 方法来显示局部变量。某些类的 .toString() 方法具有副作用,这会导致代码在调试器下运行时行为不同。此行为可在 IDE 中禁用。
元素周期表

Golang

  • append() 在容量允许的情况下会复用内存区域。向子切片追加元素可能会覆盖父切片,如果它们共享内存区域。
  • defer 在函数返回时执行,而非在词法作用域退出时执行。
  • defer 会捕获可变变量。
  • 关于 nil
  • 存在 nil 切片和空切片(两者不同)。但不存在 nil 字符串,只有空字符串。nil 映射可像空映射一样读取,但无法写入。
  • 接口 nil 的特殊行为。接口指针是包含类型信息和数据指针的胖指针。若数据指针为空但类型信息不为空,则不会等于 nil
  • 死等待。 理解 Go 中的实际并发错误
  • 不同类型的超时。 Go net/http 超时的完整指南

C/C++

  • 将元素的指针存储在 std::vector 中,然后扩展向量,vector 可能会重新分配内容,导致元素指针不再有效。
  • 由字面量字符串创建的 std::string 可能是临时对象。从临时字符串中获取 c_str() 是错误的。
  • 迭代器失效。在循环遍历容器时修改容器。
  • std::remove 不会删除元素,只是重新排列元素。erase 实际上会删除元素。
  • 以 0 开头的字面量数字将被视为八进制数。(0123 表示 83)
  • 未定义行为。编译器优化旨在保持已定义行为不变,但可自由更改未定义行为。依赖未定义行为可能导致程序在优化时崩溃。参见
    • 访问未初始化的内存属于未定义行为。将 char* 转换为结构体指针可能被视为访问未初始化的内存,因为对象的生命周期尚未开始。建议将结构体放置在其他位置并使用 memcpy 进行初始化。
    • 访问无效内存(如空指针)属于未定义行为。
    • 整数溢出/下溢是未定义行为。注意,无符号整数可能下溢到 0 以下。
  • 别名。
  • 别名意味着多个指针指向内存中的同一位置。
  • 严格别名规则:如果存在两个类型为 A*B* 的指针,则编译器假设这两个指针绝不会相等。如果它们相等,则属于未定义行为。例外情况:1. AB 之间存在子类型关系 2. 将指针转换为字节指针(char*unsigned char*std::byte*)(反向转换不适用)。
  • 进行强制转换时,安全的方法是使用 memcpystd::bit_cast
    • 未对齐的内存访问属于未定义行为。
  • 对齐。
  • 例如,64 位整数的地址必须能被 8 整除。在 ARM 架构中,以未对齐方式访问内存可能导致系统崩溃。
  • 直接将字节缓冲区的一部分视为结构体可能引发对齐问题。
  • 对齐可能导致结构体中出现填充,从而浪费空间。
    • 某些SIMD指令仅支持对齐数据。例如,AVX指令通常要求32字节对齐。

Python

  • 默认参数是一个存储值,不会在每次调用时重新创建。

SQL 数据库

  • 空值是特殊的。
    • x = null 无法正常工作。x is null 有效。空值不等于自身,与 NaN 类似。
  • 唯一索引允许重复空值(除 Microsoft SQL Server 外)。
    • select distinct 可能将空值视为相同(这取决于数据库)。
  • count(x)count(distinct x) 会忽略 x 为空值的行。
  • 日期隐式转换可能受时区影响。
  • 复杂的连接操作与 DISTINCT 结合可能比嵌套查询更慢。参见
  • 在 MySQL(InnoDB)中,如果字符串字段未设置为 character set utf8mb4,则尝试插入包含 4 字节 UTF-8 码点的文本时会引发错误。
  • MySQL(InnoDB)默认不区分大小写。
  • MySQL(InnoDB)默认支持隐式转换。select ‘123abc’ + 1; 的结果为 124。
  • MySQL(InnoDB)的间隙锁可能导致死锁。
  • 在 MySQL(InnoDB)中,您可以选择一个字段并按另一个字段分组。这会产生非确定性结果。
  • 在 SQLite 中,字段类型无关紧要,除非表设置为 strict
  • 外键可能导致隐式锁定,这可能引发死锁。
  • 锁定可能破坏可重复读隔离级别(这取决于数据库实现)。
  • 分布式 SQL 数据库可能不支持锁定或具有异常的锁定行为。这取决于数据库实现。
  • 如果后端存在 N+1 查询问题,慢查询日志中可能不会显示性能问题,因为后端会串行执行多个小型查询,而每个单独的查询速度较快。
  • 长时间运行的事务可能引发问题(例如锁定)。建议确保所有事务快速完成。
  • 全表锁定可能导致服务暂时不可用:
  • 在 MySQL(InnoDB)8.0 及更高版本中,添加唯一索引或外键操作大多是并发进行的(仅短暂锁定),不会阻塞其他操作。但在较旧版本中可能触发全表锁定。
  • 使用 mysqldump 时若未指定 --single-transaction 参数,会导致全表读锁定。
    • 在 PostgreSQL 中,create unique indexalter table ... add foreign key 会导致全表读锁。为避免此情况,可使用 create unique index concurrently 添加唯一索引。对于外键,可先执行 alter table ... add foreign key ... not valid;,再执行 alter table ... validate constraint ...
  • 关于范围:
  • 如果存储不重叠的范围,通过 select ... from ranges where p >= start and p <= end 查询包含某个点的范围是低效的(即使拥有 (start, end) 的复合索引)。高效方法:select * from (select ... from ranges where start <= p order by start desc limit 1) where end >= p(仅需start列的索引)。
  • 对于可重叠的范围,普通B-树索引无法实现高效查询。建议在MySQL中使用空间索引,在PostgreSQL中使用GiST索引。

并发与并行

  • volatile:
  • volatile本身无法替代锁。volatile本身不提供原子性。
    • 对于由锁保护的数据,无需使用 volatile。锁定操作已能建立内存顺序并防止某些错误优化。
    • 在 C/C++ 中,volatile 仅避免某些错误优化,但不会自动为 volatile 访问添加内存屏障指令。
    • 在 Java 中,volatile 访问具有顺序一致性排序(JVM 会在需要时使用内存屏障指令)
    • 在 C# 中,volatile 访问具有释放-获取排序(CLR 会在需要时使用内存屏障指令)
    • volatile 可以避免与内存读写重新排序和合并相关的错误优化。
  • 检查时与使用时(TOCTOU)。
  • 在 SQL 数据库中,对于不适合简单唯一索引的特殊唯一约束(例如跨两个表的唯一性、条件唯一性、时间范围内的唯一性),且约束由应用程序强制执行时:
    • 在 MySQL(InnoDB)中,若处于可重复读取级别,应用程序通过 select ... for update 检查后执行插入操作,且唯一性检查的列已建立索引,则可通过间隙锁定机制正常工作。(需注意间隙锁定在高并发环境下可能引发死锁,因此需确保死锁检测功能启用并采用重试机制。)
    • 在 PostgreSQL 中,如果处于可重复读取级别,应用程序使用 select ... for update 进行检查后再插入数据,这不足以在并发情况下强制执行约束(由于写入偏斜)。一些解决方案:
  • 使用可串行化级别
  • 不要依赖应用程序来强制执行约束:
  • 对于条件唯一性,使用部分唯一索引。
    * 对于跨两个表的唯一性场景,将冗余数据插入到一个额外的表中并添加唯一索引。
  • 对于时间范围排他性场景,使用范围类型和排除约束。
  • 原子引用计数(Arcshared_ptr)在多个线程频繁修改同一计数器时会变慢。参见
  • 关于读写锁:某些读写锁实现不支持从读锁升级为写锁,且在持有读锁时尝试获取写锁可能导致死锁。

许多语言中常见的问题

  • 忘记检查空值/None/nil。
  • 在循环遍历容器时修改容器。单线程“数据竞争”。
  • 意外共享可变数据。例如在 Python 中 [[0] * 10] * 10 不会创建正确的二维数组。
  • 对于非负整数,(low + high) / 2 可能发生溢出。更安全的方法是 low + (high - low) / 2
  • 短路评估。a() || b()a() 返回 true 时不会执行 b()a() && b()a() 返回 false 时不会执行 b()
  • 使用性能分析器时:性能分析器默认可能仅包含 CPU 时间,不包括等待时间。如果您的应用程序有 90% 的时间在等待数据库,火焰图可能不会包含这 90%,这会产生误导。
  • 正则表达式有许多不同的“方言”。不要假设在 JavaScript 中工作的正则表达式在 Java 中也能工作。

Linux 和 Bash

  • 如果当前目录被移动,pwd 仍显示原始路径。pwd -P 显示真实路径。
  • cmd > file 2>&1 使标准输出和标准错误都重定向到文件。但 cmd 2>&1 > file 仅使标准输出重定向到文件,而不重定向标准错误。
  • 文件名区分大小写(与 Windows 不同)。
  • 可执行文件有独立于文件权限系统的权限系统。使用 getcap 查看权限。
  • 取消设置变量。如果 DIR 未设置,rm -rf $DIR/ 将变为 rm -rf /。使用 set -u 可使 Bash 在遇到未设置变量时报错。
  • 若希望脚本向当前 shell 添加变量和别名,应通过 source script.sh 执行,而非直接执行。但 source 的效果并非永久性,且不会在重新登录后生效。可通过将其放入 ~/.bashrc 文件中实现永久生效。
  • Bash 在命令名称与命令文件路径之间存在缓存机制。若将 $PATH 中的某个文件移动,使用该命令时会引发 ENOENT 错误。可通过 hash -r 命令刷新缓存。
  • 若未对变量进行引号包裹,其换行符将被视为空格处理。
  • set -e 可使脚本在子命令失败时立即退出,但它不适用于结果被条件检查的函数内部(例如 ||&& 的左侧,或 if 语句的条件)。参见
  • K8s livenessProbe 与调试器配合使用。断点调试器通常会阻塞整个应用程序,使其无法响应健康检查请求,因此可能被 K8s livenessProbe 终止。

React

  • 在渲染代码中修改状态。
  • if 或循环内部使用钩子。
  • 值未包含在 useEffect 依赖数组中。
  • useEffect 中忘记清理。
  • 闭包陷阱(捕获过时的状态)。
  • 意外在错误的位置更改数据(不纯组件)。
  • 忘记使用 useCallback 导致不必要的重新渲染。
  • 将未缓存的值传递给缓存组件会使缓存失效。

Git

  • Rebase 会重写历史。在本地分支进行 Rebase 后,正常推送会产生异常结果(因为历史已被重写)。Rebase 应与 force push 配合使用。如果远程分支的历史记录被重写,拉取时应使用 --rebase
  • 使用 --force-with-lease 进行 force push 有时可以避免覆盖其他开发者的提交。但如果你先拉取再不进行拉取,--force-with-lease 无法提供保护。
  • 撤销合并并不会完全取消合并的副作用。如果你将 B 合并到 A,然后撤销合并,再次将 B 合并到 A 不会产生任何效果。一个解决方案是撤销合并的撤销操作。(取消合并的更干净方法,而不是撤销合并,是备份分支,然后硬重置到合并前的提交,然后挑选合并后的提交,然后强制推送。)
  • 在 GitHub 中,如果你不小心提交了机密信息(例如 API 密钥)并推送到公共仓库,即使你使用强制推送覆盖它,GitHub 仍会记录该机密信息。客座文章:如何扫描 GitHub 上所有“意外提交”以查找泄露的机密信息 示例活动标签
  • 在 GitHub 中,如果有一个私有仓库 A,你将其分叉为私有仓库 B,那么当 A 变为公开时,私有仓库 B 的内容也会公开可见,即使你删除了 B。参见
  • git stash pop 在存在冲突时不会丢弃暂存区。
  • 建议将 **/.DS_Store 添加到 .gitignore 中,因为 MacOS 会自动在每个文件夹中添加 .DS_Store 文件。

HTML 和 CSS

  • 在 flexbox 或网格布局中,min-width 的默认值为 automin-width: auto 表示最小宽度由内容决定。它比许多其他 CSS 属性具有更高的优先级,包括 flex-shrinkoverflow: hiddenwidth: 0max-width: 100%。建议设置 min-width: 0
  • 在 CSS 中,水平和垂直方向是不同的:
    • 通常 width: auto 会尝试填充父元素的可用空间。但 height: auto 通常仅尝试扩展以适应内容。
    • 对于行内元素、行内块元素和浮动元素,width: auto 不会尝试扩展。
    • margin: 0 auto 可实现水平居中。但 margin: auto 0 通常会变成 margin: 0 0,这不会实现垂直居中。在 flex-direction: column 的弹性盒布局中,margin: auto 0 可以实现垂直居中。
  • 垂直方向会发生边距合并,但水平方向不会。
  • 当布局方向翻转(例如 writing-mode: vertical-rl)时,上述情况会发生逆转。
  • 块格式化上下文(BFC):
    • display: flow-root 创建 BFC。(还有其他创建 BFC 的方法,如 overflow: hiddenoverflow: autooverflow: scrolldisplay:table,但会产生副作用)
  • 边距坍缩。两个垂直相邻的兄弟元素可以重叠边距。子元素的边距可能“溢出”到父元素之外。通过 BFC 可以避免边距坍缩。当指定 borderpadding 时,边距合并也不会发生
  • 如果父元素仅包含浮动子元素,父元素的高度将折叠为 0。可通过 BFC 修复。
  • 堆叠上下文。在以下情况下,将创建新的堆叠上下文:
    • 具有特殊渲染效果的属性(如 transformfilterperspectivemaskopacity 等)将创建新的堆叠上下文
    • position: fixedposition: sticky 将创建堆叠上下文
    • 指定 z-indexpositionabsoluterelative
    • 指定 z-index 且元素位于 flexbox 或 grid 布局中
    • isolation: isolate

    堆叠上下文可能导致以下行为:

    • z-index 在不同堆叠上下文之间无效。它仅在同一堆叠上下文中生效。
    • position: absoluteposition: fixed 的坐标基于最近的定位祖先元素。堆叠上下文的定位会影响这一点。
    • position: sticky 在不同堆叠上下文之间无效。
    • overflow: visible 仍会受到堆叠上下文的裁剪
    • background-attachment: fixed 基于堆叠上下文进行定位
  • 在移动浏览器中,当向下滚动时,顶部地址栏和底部导航栏可能超出屏幕范围。100vh 对应于顶部栏和底部栏超出屏幕时的高度,该高度大于两栏在屏幕内时的高度。现代解决方案是使用 100dvh
  • position: absolute 并非基于其父元素。它基于其最近的定位祖先(即最近具有 positionrelativeabsolute 或创建堆叠上下文的祖先)。
  • 模糊效果不考虑环境因素
  • 如果父元素的 displayflexgrid,则子元素的 float 属性无效。
  • 如果父元素的宽度/高度未预先确定,则百分比宽度/高度(例如 width: 50%height: 100%)无法正常工作。(这避免了循环依赖,即父元素的高度由内容高度决定,但内容高度又由父元素的高度决定。)
  • display: inline 会忽略 widthheight 以及 margin-topmargin-bottom
  • 空白压缩。HTML 空白处理存在问题
    • 默认情况下,HTML 中的换行符会被视为空格。多个连续的空格会压缩为一个。
    • <pre> 标签可避免空白压缩,但在内容的开头和结尾处行为异常。
    • 通常,内容开头和结尾的空格会被忽略,但在 <a> 中不会发生这种情况。
  • 两个 display: inline-block 元素之间的任何空格或换行都会被渲染为间距。这种情况在 flexbox 或 grid 中不会发生。
  • text-align 用于对齐文本和行内元素,但不会对齐块级元素(如普通 div)。
  • 默认情况下,widthheight 不包含内边距和边框。即使设置 width: 100% 并同时设置 padding: 10px,内容仍可能超出父元素的边界。通过设置 box-sizing: border-box,可以使宽度/高度包含边框和内边距。
  • 累积布局偏移。建议在 <img> 标签中明确指定 widthheight 属性,以避免因图片加载延迟导致的布局偏移。
  • 文件下载请求在 Chrome 开发者工具中不会显示,因为该工具仅显示当前标签页的网络请求,而文件下载被视为在另一个标签页中进行。要检查文件下载请求,请使用 chrome://net-export/
  • HTML 中的 JavaScript 可能干扰 HTML 解析。例如 &lt;script&gt;console.log(‘</script>’)</script> 会导致浏览器将第一个 </script> 视为结束标签。参见

Unicode 和文本编码

  • 两个概念:码点(rune)、字符集群:
    • 字母群是图形用户界面(GUI)中的“字符单位”。
  • 对于可见的 ASCII 字符,一个字符是一个代码点,一个字符是一个字母群。
  • 表情符号是一个字母群,但它可能由多个代码点组成。
  • 在 UTF-8 中,一个代码点可以是 1、2、3 或 4 个字节。字节数并不一定代表码点数。
  • 在 UTF-16 中,一个码点可以是 2 个字节或 4 个字节(代理对)。
  • 标准并未对字符集群可包含的码点数量设置上限。但出于性能考虑,实现通常会设置限制。
  • 不同语言中内存字符串的行为差异:
    • Rust 使用 UTF-8 作为内存中字符串的编码。s.len() 返回字节数。Rust 不允许直接对 str 进行索引(但允许子切片)。s.chars().count() 返回代码点数。Rust 对 UTF-8 代码点的有效性有严格要求(例如,Rust 不允许子切片在无效代码点边界处截断)。
    • Go 语言的字符串没有编码限制,与字节数组类似。字符串的长度和索引操作与字节数组相同。但最常见的编码是 UTF-8。参见
  • Java、C# 和 JS 的字符串在概念上使用 UTF-16 编码。UTF-16 以 2 字节为单位进行操作。但一个字符码点可能由 1 个 2 字节单位或 2 个 2 字节单位(代理对)组成。字符串的长度是 2 字节单位的计数,而非字符码点计数。索引操作基于 2 字节单位。
  • 在 Python 中,len(s) 返回字符码点计数。索引操作返回包含一个字符码点的字符串。
    • 在 C++ 中,std::string 没有编码限制。它可以被视为 std::vector<char> 的封装。字符串长度和索引基于字节。
  • 上述语言均不基于字母集群进行字符串长度和索引操作。
  • 某些文本文件在开头带有字节顺序标记(BOM)。例如,EF BB BF 是一个 BOM,表示文件采用 UTF-8 编码。它主要用于 Windows 系统。部分非 Windows 软件无法处理 BOM。
  • 在将二进制数据转换为字符串时,通常会将无效位置替换为 �(U+FFFD)
  • 易混淆字符
  • 规范化。例如,é 可以是 U+00E9(一个码点)或 U+0065 U+0301(两个码点)。
  • 零宽度字符不可见字符
  • 换行符。Windows 通常使用 CRLF \r\n 作为换行符。Linux 和 MacOS 通常使用 LF \n 作为换行符。
  • 汉字统一。不同语言中外观略有不同的字符可能使用相同的代码点。通常字体中会包含针对不同语言的变体,这些变体在渲染时会有所不同。选择正确的字体变体对于国际化至关重要。HTML 代码 !

浮点数

  • NaN。浮点数中的NaN不等于任何数(包括其自身)。NaN == NaN始终为假(即使位相同)。NaN != NaN始终为真。对NaN进行运算通常会得到NaN(它可能“污染”计算结果)。
  • 存在+Inf和-Inf。它们不是NaN。
  • 存在一个负零 -0.0,它与普通零不同。在浮点数比较中,负零等于零。普通零被视为“正零”。这两个零在某些计算中表现不同(例如 1.0 / +0.0 == +Inf1.0 / -0.0 == -Inf)
  • JSON 标准不允许使用 NaN 或 Inf:
    • JavaScript 的 JSON.stringify 会将 NaN 和 Inf 转换为 null。
    • Python 的 json.dumps(...) 会直接将 NaNInfinity 写入结果,这不符合 JSON 标准。json.dumps(..., allow_nan=False) 若包含 NaN 或 Inf 则会引发 ValueError
  • Go 语言的 json.Marshal 若包含 NaN 或 Inf 则会报错。
  • 直接比较浮点数的相等性可能失败。建议通过 abs(a - b) < 0.00001 等方式进行比较。
  • JavaScript 使用浮点数表示所有数字。最大“安全”整数为 253−1253−1。这里的“安全”意味着该范围内的每个整数都能准确表示。超出安全范围后,大多数整数将不准确。对于大型整数,建议使用 BigInt。如果 JSON 中包含大于该范围的整数,且 JavaScript 使用 JSON.parse 进行反序列化,结果中的数字很可能不准确。解决方法是使用其他方式反序列化 JSON 或使用字符串表示大整数。(将毫秒时间戳整数放入 JSON 是安全的,因为毫秒时间戳在公元 287396 年后会超出限制。但纳秒时间戳会遇到此问题。)
  • 由于精度损失,结合律和分配律并不严格成立。使用这些律进行矩阵乘法和求和并行化可能导致非确定性结果。示例 示例
  • 除法比乘法慢得多(除非使用近似值)。对多个数进行除法运算时,可通过先计算倒数再与倒数相乘来优化。
  • 以下因素可能导致不同硬件产生不同的浮点运算结果:
    • 硬件对FMA(融合乘加)的支持。fma(a, b, c) = a * b + c(在某些地方为a + b * c)。大多数现代硬件在FMA的中间结果具有更高精度。部分老旧硬件或嵌入式处理器不支持此功能,将其视为普通乘法和加法。
    • 浮点数具有一个 次正常数范围,用于使接近零的数值更准确。大多数现代硬件可以处理它们,但一些旧硬件和嵌入式处理器将次正常数视为零。
    • 舍入模式。标准允许使用不同的舍入模式,如舍入到最近的偶数(RNTE)或舍入到零(RTZ)。
  • 在 X86 和 ARM 架构中,舍入模式是线程局部可变状态,可通过特殊指令设置。不建议修改舍入模式,因为这可能影响其他代码。
    * 在GPU中,没有可变状态用于舍入模式。光栅化通常使用RNTE舍入模式。在CUDA中,不同的舍入模式与不同的指令相关联。
  • 不同硬件在数学函数(如sin、log)上的行为可能不同。
  • X86具有遗留FPU,其具有80位浮点寄存器和按核心的舍入模式状态。建议不要使用它们。
    • … 还有许多其他因素会导致浮点计算的差异。
  • 如何提高浮点计算精度:
    • 使计算图更扁平。例如,3层计算 a * (b * (c * d)) 可能不如2层计算 (a * b) * (c * d) 准确(具体取决于实际情况)。
    • 避免临时结果具有非常大的绝对值或非常接近零的值。
  • 利用硬件融合操作,如FMA(融合乘加)。

时间

  • 闰秒。Unix 时间戳对闰秒是“透明”的,这意味着在 Unix 时间戳与 UTC 时间之间转换时会忽略闰秒。Unix 时间戳测量的时间会在闰秒附近拉伸或压缩(闰秒模糊),以隐藏闰秒的存在。
  • 时区。UTC 和 Unix 时间戳在全球范围内是统一的,不考虑时区。但人类可读的时间是时区依赖的。建议将时间戳存储在数据库中,并在 UI 中转换为人类可读的时间,而不是将人类可读的时间存储在数据库中。
  • 夏令时(DST):在某些地区,人们会在温暖的季节将时钟向前调整一小时。
  • 由于NTP同步,时间可能“倒退”。
  • 建议将服务器的时区配置为UTC。不同节点使用不同时区会在分布式系统中引发问题。更改系统时区后,数据库可能需要重新配置或重启。
  • 系统中有两个时钟:硬件时钟和系统时钟。硬件时钟本身不关心时区。Linux 默认将其视为 UTC。Windows 默认将其视为本地时间。

网络

  • 某些路由器和防火墙会静默终止空闲的 TCP 连接,而不会通知应用程序。某些代码(如 HTTP 客户端库、数据库客户端)会维护一个 TCP 连接池以供重复使用,这些连接可能被静默无效化。为解决此问题,可配置系统 TCP 保持活动。
  • traceroute 的结果不可靠。Traceroute 并非真实。有时 tcptraceroute 可能有用。
  • TCP 慢启动会增加延迟。可通过禁用 tcp_slow_start_after_idle 解决。 参见
  • TCP 粘性数据包。Nagle 算法会延迟数据包发送,从而增加延迟。可通过启用 TCP_NODELAY 解决。参见
  • 若将后端部署在 Nginx 之后,需配置连接复用。否则在高并发情况下,由于内部端口不足,Nginx 与后端之间的连接可能失败。
  • Nginx 默认对数据包进行缓冲,这会延迟 SSE。
  • HTTP 协议并未明确禁止 GET 和 DELETE 请求携带请求体。部分场景确实会在 GET 和 DELETE 请求中使用请求体。但许多库和 HTTP 服务器不支持此功能。
  • 一个 IP 地址可托管多个网站,通过域名进行区分。HTTP 头部 Host 和 TLS 握手中的 SNI 携带域名信息,这些信息至关重要。部分网站无法通过 IP 地址访问。
  • 跨源资源共享(CORS)。对于请求其他网站(源)的请求,浏览器将阻止 JavaScript 获取响应,除非服务器响应中包含 Access-Control-Allow-Origin 头部且与客户端网站匹配。这需要配置后端。如果要将 cookie 传递到其他网站,则需要更多配置。通常,如果前端和后端位于同一网站(同一域名和端口),则不存在 CORS 问题。

其他

本文文字及图片出自 Traps to Developers

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注