首页
关于
壁纸
直播
留言
友链
统计
Search
1
《三国志英杰传》攻略
6,036 阅读
2
白嫖Emby
5,772 阅读
3
Emby客户端IOS破解
5,772 阅读
4
《吞食天地1》金手指代码
4,699 阅读
5
破解emby-server
4,041 阅读
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
登录
Search
标签搜索
ubuntu
mysql
openwrt
zerotier
springboot
centos
openvpn
jdk
吞食天地2
synology
spring
idea
windows11
吞食天地1
transmission
google-play
Japanese
xcode
群晖
kiftd
MoonjerX
累计撰写
370
篇文章
累计收到
459
条评论
首页
栏目
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
页面
关于
壁纸
直播
留言
友链
统计
搜索到
47
篇与
moonjerx
的结果
2022-12-12
别人的字节面经
如果你知道 MySQL 一行记录的存储结构,那么这个问题对你没什么难度。如果你不知道也没关系,这次我跟大家聊聊 MySQL 一行记录是怎么存储的?知道了这个之后,除了能应解锁前面这道面试题,你还会解锁这些面试题:MySQL 的 NULL 值会占用空间吗?MySQL 怎么知道 varchar(n) 实际占用数据的大小?varchar(n) 中 n 最大取值为多少?行溢出后,MySQL 是怎么处理的?这些问题看似毫不相干,其实都是在围绕「 MySQL 一行记录的存储结构」这一个知识点,所以攻破了这个知识点后,这些问题就引刃而解了。MySQL 的数据存放在哪个文件?大家都知道 MySQL 的数据都是保存在磁盘的,那具体是保存在哪个文件呢?MySQL 存储的行为是由存储引擎实现的,MySQL 支持多种存储引擎,不同的存储引擎保存的文件自然也不同。InnoDB 是我们常用的存储引擎,也是 MySQL 默认的存储引擎。所以,本文主要以 InnoDB 存储引擎展开讨论。先来看看 MySQL 数据库的文件存放在哪个目录?mysql> SHOW VARIABLES LIKE 'datadir'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | datadir | /var/lib/mysql/ | +---------------+-----------------+ 1 row in set (0.00 sec)我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。比如,我这里有一个名为 my_test 的 database,该 database 里有一张名为 t_order 数据库表。然后,我们进入 /var/lib/mysql/my_test 目录,看看里面有什么文件?[root@xiaolin ~]#ls /var/lib/mysql/my_test db.opt t_order.frm t_order.ibd可以看到,共有三个文件,这三个文件分别代表着:db.opt,用来存储当前数据库的默认字符集和字符校验规则。t_order.frm ,t_order 的表结构会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。t_order.ibd,t_order 的表数据会保存在这个文件。表数据既可以存在共享表空间文件(文件名:ibdata1)里,也可以存放在独占表空间文件(文件名:表名字.idb)。这个行为是由参数 innodb_file_per_table 控制的,若设置了参数 innodb_file_per_table 为 1,则会将存储的数据、索引等信息单独存储在一个独占表空间,从 MySQL 5.6.6 版本开始,它的默认值就是 1 了,因此从这个版本之后, MySQL 中每一张表的数据都存放在一个独立的 .idb 文件。好了,现在我们知道了一张数据库表的数据是保存在「 表名字.idb 」的文件里的,这个文件也称为独占表空间文件。那这个表空间文件的结构是怎么样的?表空间由段(segment)、区(extent)、页(page)、行(row)组成 ,InnoDB存储引擎的逻辑存储结构大致如下图:下面我们从下往上一个个看看。1、行(row)数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。后面我们详细介绍 InnoDB 存储引擎的行格式,也是本文重点介绍的内容。2、页(page)记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。因此,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。页是 InnoDB 存储引擎磁盘管理的最小单元,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。页的类型有很多,常见的有数据页、undo 日志页、溢出页等等。数据表中的行记录是用「数据页」来管理的,数据页的结构这里我就不讲细说了,之前文章有说过,感兴趣的可以去看这篇文章:换一个角度看 B+ 树总之知道表中的记录存储在「数据页」里面就行。3、区(extent)我们知道 InnoDB 存储引擎是用 B+ 树来组织数据的。B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子节点)的时候性能就会很高。那具体怎么解决呢?在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。4、段(segment)表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。索引段:存放 B + 树的非叶子节点的区的集合;数据段:存放 B + 树的叶子节点的区的集合;回滚段:存放的是回滚数据的区的集合,之前讲事务隔离的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。好了,终于说完表空间的结构了。接下来,就具体讲一下 InnoDB 的行格式了。之所以要绕一大圈才讲行记录的格式,主要是想让大家知道行记录是存储在哪个文件,以及行记录在这个表空间文件中的哪个区域,有一个从上往下切入的视角,这样理解起来不会觉得很抽象。InnoDB 行格式有哪些?行格式(row_format),就是一条记录的存储结构。InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。Redundant 是很古老的行格式了, MySQL 5.0 版本之前用的行格式,现在基本没人用了。由于 Redundant 不是一种紧凑的行格式,所以 MySQL 5.0 之后引入了 Compact 行记录存储方式,Compact 是一种紧凑的行格式,设计的初衷就是为了让一个数据页中可以存放更多的行记录,从 MySQL 5.1 版本之后,行格式默认设置成 Compact。Dynamic 和 Compressed 两个都是紧凑的行格式,它们的行格式都和 Compact 差不多,因为都是基于 Compact 改进一点东西。从 MySQL5.7 版本之后,默认使用 Dynamic 行格式。Redundant 行格式我这里就不讲了,因为现在基本没人用了,这次重点介绍 Compact 行格式,因为 Dynamic 和 Compressed 这两个行格式跟 Compact 非常像。所以,弄懂了 Compact 行格式,之后你们在去了解其他行格式,很快也能看懂。COMPACT 行格式长什么样?先跟 Compact 行格式混个脸熟,它长这样:可以看到,一条完整的记录分为「记录的额外信息」和「记录的真实数据」两个部分。接下里,分别详细说下。记录的额外信息记录的额外信息包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。1. 变长字段长度列表varchar(n) 和 char(n) 的区别是什么,相信大家都非常清楚,char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)不固定的。所以,在存储数据的时候要把这些数据占用的字节数也存起来,存到「变长字段长度列表」里面,读取数据的时候才能根据这个「变长字段长度列表」去读取对应长度的数据。其他 TEXT、BLOB 等变长字段也是这么实现的。为了展示「变长字段长度列表」具体是怎么保存变长字段占用的字节数,我们先创建这样一张表,字符集是 ascii(所以每一个字符占用的 1 字节),行格式是 Compact,t_user 表中 name 和 phone 字段都是变长字段:CREATE TABLE `t_user` ( `id` int(11) NOT NULL, `name` VARCHAR(20) NOT NULL, `phone` VARCHAR(20) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;现在 t_user 表里有这三条记录:接下来,我们看看看看这三条记录的行格式中的 「变长字段长度列表」是怎样存储的。先来看第一条记录:name 列的值为 a,长度是 1 字节,十六进制 0x01phone 列的值为 123,长度是 3 字节,十六进制 0x03age 列和 id 列不是变长字段,所以这里不用管。这些变长字段的长度值会按照列的顺序逆序存放(等下会说为什么要这么设计),所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。同样的道理,我们也可以得出第二条记录的行格式中,「变长字段长度列表」里的内容是「 04 02」,如下图:第三条记录中 phone 列的值是 NULL,NULL 是不会存放在行格式中记录的真实数据部分里的,所以「变长字段长度列表」里不需要保存值为 NULL 的变长字段的长度。为什么「变长字段长度列表」的信息要按照逆序存放?这个设计是有想法的,主要是因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。「变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。同样的道理, NULL 值列表的信息也需要逆序存放。如果你不知道什么是 CPU Cache,可以看这篇文章:面试官:如何写出让 CPU 跑得更快的代码?,这属于计算机组成的知识。每个数据库表的行格式都有「变长字段字节数列表」吗?其实变长字段字节数列表不是必须的。当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了,因为没必要,不如去掉以节省空间。所以「变长字段长度列表」只出现在数据表有变长字段的时候。2. NULL 值列表表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。二进制位的值为1时,代表该列的值为NULL。二进制位的值为0时,代表该列的值不为NULL。另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0。还是以 t_user 表的这三条记录作为例子:接下来,我们看看看看这三条记录的行格式中的 NULL 值列表是怎样存储的。先来看第一条记录,第一条记录所有列都有值,不存在 NULL 值,所以用二进制来表示是酱紫的:但是 InnoDB 是用整数字节的二进制位来表示NULL值列表的,现在不足 8 位,所以要在高位补 0,最终用二进制来表示是酱紫的:所以,对于第一条数据,NULL 值列表用十六进制表示是 0x00。接下来看第二条记录,第二条记录 age 列是 NULL 值,所以,对于第二条数据,NULL值列表用十六进制表示是 0x04。最后第三条记录,第三条记录 phone 列 和 age 列是 NULL 值,所以,对于第三条数据,NULL 值列表用十六进制表示是 0x06。我们把三条记录的 NULL 值列表都填充完毕后,它们的行格式是这样的:每个数据库表的行格式都有「NULL 值列表」吗?NULL 值列表也不是必须的。当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以节省 1 字节的空间(NULL 值列表占用 1 字节空间)。3. 记录头信息记录头信息中包含的内容很多,我就不一一列举了,这里说几个比较重要的:delete_mask :标识此条数据是否被删除。从这里可以知道,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。next_record:下一条记录的位置。从这里可以知道,记录与记录之间是通过链表组织的。在前面我也提到了,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录 记录的真实数据 记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer,我们来看下这三个字段是什么。row_id如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。trx_id事务id,表示这个数据是由哪个事务生成的。trx_id是必需的,占用 6 个字节。roll_pointer这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。如果你熟悉 MVCC 机制,你应该就清楚 trx_id 和 roll_pointer 的作用了。varchar(n) 中 n 最大取值为多少?varchar(n) 字段类型的 n 代表的是最多存储的字符数量,那 n 最大能设置多少?这个问题要考虑两个因素:行格式中「变长字段长度列表」最大能表示多少字节?知道了这个才能知道,一行数据最大能存储多少字节的数据。数据库表的字符集,确定了这个,才能知道 1 个字符占用多少字节。行格式中「变长字段长度列表」有时候是占用 1 字节,有时候是占用 2 字节:如果变长字段允许存储的最大字节数小于等于 255 字节,「变长字段长度列表」就占用 1 个字节;如果变长字段允许存储的最大字节数大于 255 字节,「变长字段长度列表」就占用 2 个字节;可以看到,「 变长字段长度列表」占用的字节数最大不会不超过 2 字节。 2 个字节的最大值是 65535(十进制),从这里可以推测一行记录最大能存储 65535 字节的数据,实际上真的是这样吗?我这里以 ascii 字符集作为例子,这意味着 1 个字符占用 1 字节。那么 varchar(65535) 就意味着最多可存储 65535 个 ascii 字符,刚好满足一行记录最大能存储 65535 字节的数据。我们定义一个 varchar(65535) 类型的字段,字符集为 ascii 的数据库表。CREATE TABLE test ( `name` VARCHAR(65535) NULL ) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;看能不能成功创建一张表:可以看到,创建失败了。从报错信息就可以知道一行数据的最大字节数是 65535(不包含 TEXT、BLOBs 这种大对象类型),其中包含了 storage overhead。问题来了,这个 storage overhead 是什么呢?其实就是变长字段长度列表和 NULL 值列表,也就是说一行数据的最大字节数 65535,其实是包含「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。我们存储字段类型为 varchar(n) 的数据时,其实分成了三个部分来存储:真实数据真实数据占用的字节数NULL 标识,如果不允许为NULL,这部分不需要前面我创建表的时候,字段是允许为 NULL 的,所以会占用 1 字节来存储 NULL 标识,字段是变长字段且变长字段允许存储的最大字节数大于 255 字节 ,所以会占用 2 字节存储真实数据的占用的字节数,所以最多可以存储 65535- 2 - 1 = 65532 个字节。我们先来测试看看 varchar(65533) 是否可行?可以看到,还是不行,接下来看看 varchar(65532) 是否可行?可以看到,创建成功了。当然,我上面这个例子是针对字符集为 ascii 情况,如果采用的是 UTF-8,varchar(n) 最多能存储的数据计算方式就不一样了:在 UTF-8 字符集下,一个字符串最多需要三个字节,varchar(n) 的 n 最大取值就是 65532/3 = 21844。上面所说的只是针对于一个字段的计算方式。如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。行溢出后,MySQL 是怎么处理的?MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。大致如下图所示。上面这个是 Compact 行格式在发生行溢出后的处理。Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中,看起来就像下面这样:总结MySQL 的 NULL 值是怎么存放的?MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。MySQL 怎么知道 varchar(n) 实际占用数据的大小?MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。varchar(n) 中 n 最大取值为多少?一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL值列表所占用的字节数 = 65535 - 2 - 1 = 65532行溢出后,MySQL 是怎么处理的?如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
2022年12月12日
109 阅读
0 评论
0 点赞
2022-11-21
此内容被密码保护
加密文章,请前往内页查看详情
2022年11月21日
3 阅读
0 评论
0 点赞
2022-08-31
“ 12306 ” 的架构
限并发带来的思考虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。尤其是春节期间,大家不仅使用 12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。“12306 服务”承受着这个世界上任何秒杀系统都无法超越的 QPS,上百万的并发再正常不过了!笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在 100 万人同时抢 1 万张火车票时,系统提供正常、稳定的服务。大型高并发系统架构高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。下边是一个简单的示意图:负载均衡简介上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡。① OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称 IGP)OSPF 通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF 会自动计算路由接口上的 Cost 值,但也可以通过手工指定该接口的 Cost 值,手工指定的优先于自动计算的值。OSPF 计算的 Cost,同样是和接口带宽成反比,带宽越高,Cost 值越小。到达目标相同 Cost 值的路径,可以执行负载均衡,最多 6 条链路同时执行负载均衡。②LVS (Linux Virtual Server)它是一种集群(Cluster)技术,采用 IP 负载均衡技术和基于内容请求分发技术。调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。③Nginx想必大家都很熟悉了,是一款非常高性能的 HTTP 代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。Nginx 实现负载均衡的方式主要有三种:轮询加权轮询IP Hash 轮询下面我们就针对 Nginx 的加权轮询做专门的配置和测试。Nginx 加权轮询的演示Nginx 实现负载均衡通过 Upstream 模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。下面是一个加权轮询负载的配置,我将在本地的监听 3001-3004 端口,分别配置 1,2,3,4 的权重:#配置负载均衡 upstream load_rule { server 127.0.0.1:3001 weight=1; server 127.0.0.1:3002 weight=2; server 127.0.0.1:3003 weight=3; server 127.0.0.1:3004 weight=4; } ... server { listen 80; server_name load_balance.com www.load_balance.com; location / { proxy_pass http://load_rule; }我在本地 /etc/hosts 目录下配置了 www.load_balance.com 的虚拟域名地址。接下来使用 Go 语言开启四个 HTTP 端口监听服务,下面是监听在 3001 端口的 Go 程序,其他几个只需要修改端口即可:package main import ( "net/http" "os" "strings" ) func main() { http.HandleFunc("/buy/ticket", handleReq) http.ListenAndServe(":3001", nil) } //处理请求函数,根据请求将响应结果信息写入日志 func handleReq(w http.ResponseWriter, r *http.Request) { failedMsg := "handle in port:" writeLog(failedMsg, "./stat.log") } //写入日志 func writeLog(msg string, logPath string) { fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) defer fd.Close() content := strings.Join([]string{msg, "\r\n"}, "3001") buf := []byte(content) fd.Write(buf) }我将请求的端口日志信息写到了 ./stat.log 文件当中,然后使用 AB 压测工具做压测:ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket 统计日志中的结果,3001-3004 端口分别得到了 100、200、300、400 的请求量。这和我在 Nginx 中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。具体的实现大家可以参考 Nginx 的 Upsteam 模块实现源码:https://www.kancloud.cn/digest/understandingnginx/202607秒杀抢购系统选型回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢?从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的 QPS 也是非常高的。如何将单机性能优化到极致呢?要解决这个问题,我们就要想明白一件事:通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段。我们系统要做的事情是要保证火车票订单不超卖、不少卖,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。这三个阶段的先后顺序该怎么分配才更加合理呢?我们来分析一下:下单减库存当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。但是这样也会产生一些问题:在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的。如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制 IP 和用户的购买订单数量,这也不算是一个好方法。支付减库存如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单。当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘 IO。预扣库存从上边两种方案的考虑,我们可以得出结论:只要创建订单,就要频繁操作数据库 IO。那么有没有一种不需要直接操作数据库 IO 的方案呢,这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办?我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。订单的生成是异步的,一般都会放到 MQ、Kafka 这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。扣库存的艺术从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?另外,搜索公众号GitHub猿后台回复“赚钱”,获取一份惊喜礼包。在单机低并发情况下,我们实现扣库存通常是这样的:为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多 IO,对数据库的操作又是阻塞的。这种方式根本不适合高并发的秒杀系统。接下来我们对单机扣库存的方案做优化:本地扣库存。我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。改进过之后的单机系统是这样的:这样就避免了对数据库频繁的 IO 操作,只在内存中做运算,极大的提高了单机抗并发的能力。但是百万的用户请求量单机是无论如何也抗不住的,虽然 Nginx 处理网络请求使用 Epoll 模型,c10k 的问题在业界早已得到了解决。但是 Linux 系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。上面我们提到了 Nginx 的加权均衡策略,我们不妨假设将 100W 的用户请求量平均均衡到 100 台服务器上,这样单机所承受的并发量就小了很多。然后我们每台机器本地库存 100 张火车票,100 台服务器上的总库存还是 1 万,这样保证了库存订单不超卖,下面是我们描述的集群架构:问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这 100 台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“Buffer 库存”用来防止机器中有机器宕机的情况。我们结合下面架构图具体分析一下:我们采用 Redis 存储统一库存,因为 Redis 的性能非常高,号称单机 QPS 能抗 10W 的并发。在本地减库存以后,如果本地有订单,我们再去请求 Redis 远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。当机器中有机器宕机时,因为每个机器上有预留的 Buffer 余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。Buffer 余票设置多少合适呢,理论上 Buffer 设置的越多,系统容忍宕机的机器数量就越多,但是 Buffer 设置的太大也会对 Redis 造成一定的影响。虽然 Redis 内存数据库抗并发能力非常高,请求依然会走一次网络 IO,其实抢票过程中对 Redis 的请求次数是本地库存和 Buffer 库存的总量。因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑。这在一定程度上也避免了巨大的网络请求量把 Redis 压跨,所以 Buffer 值设置多少,需要架构师对系统的负载能力做认真的考量。代码演示Go 语言原生为并发设计,我采用 Go 语言给大家演示一下单机抢票的具体流程。初始化工作Go 包中的 Init 函数先于 Main 函数执行,在这个阶段主要做一些准备性工作。我们系统需要做的准备工作有:初始化本地库存、初始化远程 Redis 存储统一库存的 Hash 键值、初始化 Redis 连接池。另外还需要初始化一个大小为 1 的 Int 类型 Chan,目的是实现分布式锁的功能。也可以直接使用读写锁或者使用 Redis 等其他的方式避免资源竞争,但使用 Channel 更加高效,这就是 Go 语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存。Redis 库使用的是 Redigo,下面是代码实现:... //localSpike包结构体定义 package localSpike type LocalSpike struct { LocalInStock int64 LocalSalesVolume int64 } ... //remoteSpike对hash结构的定义和redis连接池 package remoteSpike //远程订单存储健值 type RemoteSpikeKeys struct { SpikeOrderHashKey string //redis中秒杀订单hash结构key TotalInventoryKey string //hash结构中总订单库存key QuantityOfOrderKey string //hash结构中已有订单数量key } //初始化redis连接池 func NewPool() *redis.Pool { return &redis.Pool{ MaxIdle: 10000, MaxActive: 12000, // max number of connections Dial: func() (redis.Conn, error) { c, err := redis.Dial("tcp", ":6379") if err != nil { panic(err.Error()) } return c, err }, } } ... func init() { localSpike = localSpike2.LocalSpike{ LocalInStock: 150, LocalSalesVolume: 0, } remoteSpike = remoteSpike2.RemoteSpikeKeys{ SpikeOrderHashKey: "ticket_hash_key", TotalInventoryKey: "ticket_total_nums", QuantityOfOrderKey: "ticket_sold_nums", } redisPool = remoteSpike2.NewPool() done = make(chan int, 1) done <- 1 }本地扣库存和统一扣库存本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回 Bool 值:package localSpike //本地扣库存,返回bool值 func (spike *LocalSpike) LocalDeductionStock() bool{ spike.LocalSalesVolume = spike.LocalSalesVolume + 1 return spike.LocalSalesVolume < spike.LocalInStock }注意这里对共享数据 LocalSalesVolume 的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用 Channel 来实现,这块后边会讲。统一扣库存操作 Redis,因为 Redis 是单线程的,而我们要实现从中取数据,写数据并计算一些列步骤,我们要配合 Lua 脚本打包命令,保证操作的原子性:package remoteSpike ...... const LuaScript = ` local ticket_key = KEYS[1] local ticket_total_key = ARGV[1] local ticket_sold_key = ARGV[2] local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key)) local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key)) -- 查看是否还有余票,增加订单数量,返回结果值 if(ticket_total_nums >= ticket_sold_nums) then return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1) end return 0 ` //远端统一扣库存 func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool { lua := redis.NewScript(1, LuaScript) result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey)) if err != nil { return false } return result != 0 }我们使用 Hash 结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的 Bool 值。在启动服务之前,我们需要初始化 Redis 的初始库存信息:hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0响应用户信息我们开启一个 HTTP 服务,监听在一个端口上:package main ... func main() { http.HandleFunc("/buy/ticket", handleReq) http.ListenAndServe(":3005", nil) }上面我们做完了所有的初始化工作,接下来 handleReq 的逻辑非常清晰,判断是否抢票成功,返回给用户信息就可以了。package main //处理请求函数,根据请求将响应结果信息写入日志 func handleReq(w http.ResponseWriter, r *http.Request) { redisConn := redisPool.Get() LogMsg := "" <-done //全局读写锁 if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) { util.RespJson(w, 1, "抢票成功", nil) LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10) } else { util.RespJson(w, -1, "已售罄", nil) LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10) } done <- 1 //将抢票状态写入到log中 writeLog(LogMsg, "./stat.log") } func writeLog(msg string, logPath string) { fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) defer fd.Close() content := strings.Join([]string{msg, "\r\n"}, "") buf := []byte(content) fd.Write(buf) }前边提到我们扣库存时要考虑竞态条件,我们这里是使用 Channel 避免并发的读写,保证了请求的高效顺序执行。我们将接口的返回信息写入到了 ./stat.log 文件方便做压测统计。单机服务压测开启服务,我们使用 AB 压测工具进行测试: ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket 下面是我本地低配 Mac 的压测信息:This is ApacheBench, Version 2.3 <$revision: 1826891=""> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: Server Hostname: 127.0.0.1 Server Port: 3005 Document Path: /buy/ticket Document Length: 29 bytes Concurrency Level: 100 Time taken for tests: 2.339 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 1370000 bytes HTML transferred: 290000 bytes Requests per second: 4275.96 [#/sec] (mean) Time per request: 23.387 [ms] (mean) Time per request: 0.234 [ms] (mean, across all concurrent requests) Transfer rate: 572.08 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 8 14.7 6 223 Processing: 2 15 17.6 11 232 Waiting: 1 11 13.5 8 225 Total: 7 23 22.8 18 239 Percentage of the requests served within a certain time (ms) 50% 18 66% 24 75% 26 80% 28 90% 33 95% 39 98% 45 99% 54 100% 239 (longest request)根据指标显示,我单机每秒就能处理 4000+ 的请求,正常服务器都是多核配置,处理 1W+ 的请求根本没有问题。而且查看日志发现整个服务过程中,请求都很正常,流量均匀,Redis 也很正常://stat.log ... result:1,localSales:145 result:1,localSales:146 result:1,localSales:147 result:1,localSales:148 result:1,localSales:149 result:1,localSales:150 result:0,localSales:151 result:0,localSales:152 result:0,localSales:153 result:0,localSales:154 result:0,localSales:156 ...总结回顾总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对 DB 数据库 IO 的操作。对 Redis 网络 IO 的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。我觉得其中有两点特别值得学习总结:①负载均衡,分而治之通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致。这样系统的整体也就能承受极高的并发了,就像工作的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。②合理的使用并发和异步自 Epoll 网络架构模型解决了 c10k 问题以来,异步越来越被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果。这点在 Nginx、Node.JS、Redis 上都能体现,他们处理网络请求使用的 Epoll 模型,用实践告诉了我们单线程依然可以发挥强大的威力。服务器已经进入了多核时代,Go 语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如 Go 处理 HTTP 请求时每个请求都会在一个 Goroutine 中执行。{card-describe title="版权申明"}内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!{/card-describe}
2022年08月31日
96 阅读
0 评论
0 点赞
2022-08-17
Ubuntu系统设置时区时间
1、首先输入date -R查看当下系统时间是否一致。2、设置时区,亚洲/上海timedatectl set-timezone Asia/Shanghai3、设置时区时间格式第一行是日期格式,第二行是时间格式sudo date -s MM/DD/YY sudo date -s hh:mm:ss 4、将当前时间写入BIOS避免重启之后失效sudo hwclock --systohc5、再次查看日期是否更改成功date -R
2022年08月17日
118 阅读
0 评论
0 点赞
2022-08-17
Tailscale 开源版中文部署指南
支持无限设备数、自定义多网段 、自建中继等高级特性原文链接🔗 https://fuckcloudnative.io/posts/how-to-set-up-or-migrate-headscale/目前国家工信部在大力推动三大运营商发展 IPv6,对家用宽带而言,可以使用的 IPv4 公网 IP 会越来越少。有部分地区即使拿到了公网 IPv4 地址,也是个大内网地址,根本不是真正的公网 IP,访问家庭内网的资源将会变得越来越困难。部分小伙伴可能会选择使用 frp 等针对特定协议和端口的内网穿透方案,但这种方案还是不够酸爽,无法访问家庭内网任意设备的任意端口。更佳的选择还是通过 VPN 来组建大内网。至于该选择哪种 VPN,毫无疑问肯定是 WireGuard,WireGuard 就是 VPN 的未来。我已经不止一次向大家推荐使用 WireGuard 了,我累了,不想再讲了,你爱 JB 用辣鸡 OpenVPN 之类的就用吧,你开心就好。WireGuard 相比于传统 VPN 的核心优势是没有 VPN 网关,所有节点之间都可以点对点(P2P)连接,也就是我之前提到的全互联模式(full mesh)[1],效率更高,速度更快,成本更低。WireGuard 目前最大的痛点就是上层应用的功能不够健全,因为 WireGuard 推崇的是 Unix 的哲学,WireGuard 本身只是一个内核级别的模块,只是一个数据平面,至于上层的更高级的功能(比如秘钥交换机制,UDP 打洞,ACL 等),需要通过用户空间的应用来实现。所以为了基于 WireGuard 实现更完美的 VPN 工具,现在已经涌现出了很多项目在互相厮杀。笔者前段时间一直在推崇 Netmaker[2],它通过可视化界面来配置 WireGuard 的全互联模式,它支持 UDP 打洞、多租户等各种高端功能,几乎适配所有平台,非常强大。然而现实世界是复杂的,无法保证所有的 NAT 都能打洞成功,且 Netmaker 目前还没有 fallback 机制,如果打洞失败,无法 fallback 改成走中继节点。Tailscale 在这一点上比 Netmaker 高明许多,它支持 fallback 机制,可以尽最大努力实现全互联模式,部分节点即使打洞不成功,也能通过中继节点在这个虚拟网络中畅通无阻。没错,我移情别恋了,从 Netmaker 阵营转向了 Tailscale,是渣男没错了。Tailscale 是什么Tailscale 是一种基于 WireGuard 的虚拟组网工具,和 Netmaker 类似,最大的区别在于 Tailscale 是在用户态实现了 WireGuard 协议,而 Netmaker 直接使用了内核态的 WireGuard。所以 Tailscale 相比于内核态 WireGuard 性能会有所损失,但与 OpenVPN 之流相比还是能甩好几十条街的,Tailscale 虽然在性能上做了些许取舍,但在功能和易用性上绝对是完爆其他工具:开箱即用无需配置防火墙没有额外的配置高安全性/私密性自动密钥轮换点对点连接支持用户审查端到端的访问记录在原有的 ICE、STUN 等 UDP 协议外,实现了 DERP TCP 协议来实现 NAT 穿透基于公网的控制服务器下发 ACL 和配置,实现节点动态更新通过第三方(如 Google) SSO 服务生成用户和私钥,实现身份认证简而言之,我们可以将 Tailscale 看成是更为易用、功能更完善的 WireGuard。光有这些还不够,作为一个白嫖党,咱更关心的是免费与开源。Tailscale 是一款商业产品,但个人用户是可以白嫖的,个人用户在接入设备不超过 20 台的情况下是可以免费使用的(虽然有一些限制,比如子网网段无法自定义,且无法设置多个子网)。除 Windows 和 macOS 的图形应用程序外,其他 Tailscale 客户端的组件(包含 Android 客户端)是在 BSD 许可下以开源项目的形式开发的,你可以在他们的 GitHub 仓库[3]找到各个操作系统的客户端源码。对于大部份用户来说,白嫖 Tailscale 已经足够了,如果你有更高的需求,比如自定义网段,可以选择付费。我就不想付费行不行?行,不过得往下看。Headscale 是什么Headscale 部署很简单,推荐直接在 Linux 主机上安装。理论上来说只要你的 Headscale 服务可以暴露到公网出口就行,但最好不要有 NAT,所以推荐将 Headscale 部署在有公网 IP 的云主机上。首先需要到其 GitHub 仓库的 Release 页面下载最新版的二进制文件。$ wget --output-document=/usr/local/bin/headscale \ https://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH> $ chmod +x /usr/local/bin/headscale创建配置目录:$ mkdir -p /etc/headscale创建目录用来存储数据与证书:$ mkdir -p /var/lib/headscale创建空的 SQLite 数据库文件:$ touch /var/lib/headscale/db.sqlite创建 Headscale 配置文件:$ wget https://github.com/juanfont/headscale/raw/main/config-example.yaml -O /etc/headscale/config.yaml修改配置文件,将 server_url 改为公网 IP 或域名。如果是国内服务器,域名必须要备案。我的域名无法备案,所以我就直接用公网 IP 了。如果暂时用不到 DNS 功能,可以先将 magic_dns 设为 false。可自定义私有网段,也可同时开启 IPv4 和 IPv6:ip_prefixes: # - fd7a:115c:a1e0::/48 - 10.1.0.0/16创建 SystemD service 配置文件:# /etc/systemd/system/headscale.service [Unit] Description=headscale controller After=syslog.target After=network.target [Service] Type=simple User=headscale Group=headscale ExecStart=/usr/local/bin/headscale serve Restart=always RestartSec=5 # Optional security enhancements NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/var/lib/headscale /var/run/headscale AmbientCapabilities=CAP_NET_BIND_SERVICE RuntimeDirectory=headscale [Install] WantedBy=multi-user.target创建 headscale 用户:$ useradd headscale -d /home/headscale -m修改 /var/lib/headscale 目录的 owner:$ chown headscale:headscale /var/lib/headscale修改配置文件中的 unix_socket:unix_socket: /var/run/headscale/headscale.sockReload SystemD 以加载新的配置文件:$ systemctl daemon-reload启动 Headscale 服务并设置开机自启:$ systemctl enable --now headscale查看运行状态:$ systemctl status headscale查看占用端口:$ ss -tulnp|grep headscale tcp LISTEN 0 1024 [::]:9090 [::]:* users:(("headscale",pid=10899,fd=13)) tcp LISTEN 0 1024 [::]:50443 [::]:* users:(("headscale",pid=10899,fd=10)) tcp LISTEN 0 1024 [::]:8080 [::]:* users:(("headscale",pid=10899,fd=12))Tailscale 中有一个概念叫 tailnet,你可以理解成租户,租户与租户之间是相互隔离的,具体看参考 Tailscale 的官方文档:What is a tailnet[5]。Headscale 也有类似的实现叫 namespace,即命名空间。我们需要先创建一个 namespace,以便后续客户端接入,例如:$ headscale namespaces create default查看命名空间:$ headscale namespaces list ID | Name | Created 1 | default | 2022-03-09 06:12:06Tailscale 客户端接入目前除了 iOS 客户端,其他平台的客户端都有办法自定义 Tailscale 的控制服务器。OS是否支持 HeadscaleLinuxYesOpenBSDYesFreeBSDYesmacOSYesWindowsYes 参考 Windows 客户端文档[6]Android需要自己编译客户端[7]iOS暂不支持我们先来看下 Linux 平台的接入。LinuxTailscale 官方提供了各种 Linux 发行版的软件包,但国内的网络你懂得,软件源根本用不了。好在官方还提供了静态编译的二进制文件[8],我们可以直接下载。例如:$ wget https://pkgs.tailscale.com/stable/tailscale_1.22.2_amd64.tgz解压:$ tar zxvf tailscale_1.22.2_amd64.tgz x tailscale_1.22.2_amd64/ x tailscale_1.22.2_amd64/tailscale x tailscale_1.22.2_amd64/tailscaled x tailscale_1.22.2_amd64/systemd/ x tailscale_1.22.2_amd64/systemd/tailscaled.defaults x tailscale_1.22.2_amd64/systemd/tailscaled.service将二进制文件复制到官方软件包默认的路径下:$ cp tailscale_1.22.2_amd64/tailscaled /usr/sbin/tailscaled $ cp tailscale_1.22.2_amd64/tailscale /usr/bin/tailscale将 systemD service 配置文件复制到系统路径下:$ cp tailscale_1.22.2_amd64/systemd/tailscaled.service /lib/systemd/system/tailscaled.service将环境变量配置文件复制到系统路径下:$ cp tailscale_1.22.2_amd64/systemd/tailscaled.defaults /etc/default/tailscaled启动 tailscaled.service 并设置开机自启:$ systemctl enable --now tailscaled查看服务状态:$ systemctl status tailscaledTailscale 接入 Headscale:# 将 <HEADSCALE_PUB_IP> 换成你的 Headscale 公网 IP 或域名 $ tailscale up --login-server=http://<HEADSCALE_PUB_IP>:8080 --accept-routes=true --accept-dns=false这里推荐将 DNS 功能关闭,因为它会覆盖系统的默认 DNS。如果你对 DNS 有需求,可自己研究官方文档,这里不再赘述。执行完上面的命令后,会出现下面的信息:To authenticate, visit: http://xxxxxx:8080/register?key=905cf165204800247fbd33989dbc22be95c987286c45aac3033937041150d846在浏览器中打开该链接,就会出现如下的界面:将其中的命令复制粘贴到 headscale 所在机器的终端中,并将 NAMESPACE 替换为前面所创建的 namespace。$ headscale -n default nodes register --key 905cf165204800247fbd33989dbc22be95c987286c45aac3033937041150d846 Machine register注册成功,查看注册的节点:$ headscale nodes list ID | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired 1 | coredns | [Ew3RB] | default | 10.1.0.1 | false | 2022-03-20 09:08:58 | online | no回到 Tailscale 客户端所在的 Linux 主机,可以看到 Tailscale 会自动创建相关的路由表和 iptables 规则。路由表可通过以下命令查看:$ ip route show table 52查看 iptables 规则:$ iptables -S -P INPUT DROP -P FORWARD ACCEPT -P OUTPUT ACCEPT -N ts-forward -N ts-input -A INPUT -j ts-input -A FORWARD -j ts-forward -A ts-forward -i tailscale0 -j MARK --set-xmark 0x40000/0xffffffff -A ts-forward -m mark --mark 0x40000 -j ACCEPT -A ts-forward -s 100.64.0.0/10 -o tailscale0 -j DROP -A ts-forward -o tailscale0 -j ACCEPT -A ts-input -s 10.1.0.5/32 -i lo -j ACCEPT -A ts-input -s 100.115.92.0/23 ! -i tailscale0 -j RETURN -A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j DROP $ iptables -S -t nat -P PREROUTING ACCEPT -P INPUT ACCEPT -P OUTPUT ACCEPT -P POSTROUTING ACCEPT -A ts-postrouting -m mark --mark 0x40000 -j MASQUERADEmacOSmacOS 客户端的安装相对来说就简单多了,只需要在应用商店安装 APP 即可,前提是你需要一个美区 ID。。。安装完成后还需要做一些骚操作,才能让 Tailscale 使用 Headscale 作为控制服务器。当然,Headscale 已经给我们提供了详细的操作步骤,你只需要在浏览器中打开 URL:http://<HEADSCALE_PUB_IP>:8080/apple,便会出现如下的界面:你只需要按照图中所述的步骤操作即可,本文就不再赘述了。修改完成后重启 Tailscale 客户端,在 macOS 顶部状态栏中找到 Tailscale 并点击,然后再点击 Log in。然后立马就会跳转到浏览器并打开一个页面。接下来与之前 Linux 客户端相同,回到 Headscale 所在的机器执行浏览器中的命令即可,注册成功:回到 Headscale 所在主机,查看注册的节点:$ headscale nodes list ID | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired 1 | coredns | [Ew3RB] | default | 10.1.0.1 | false | 2022-03-20 09:08:58 | online | no 2 | carsondemacbook-pro | [k7bzX] | default | 10.1.0.2 | false | 2022-03-20 09:48:30 | online | no回到 macOS,测试是否能 ping 通对端节点:$ ping -c 2 10.1.0.1 PING 10.1.0.1 (10.1.0.1): 56 data bytes 64 bytes from 10.1.0.1: icmp_seq=0 ttl=64 time=37.025 ms 64 bytes from 10.1.0.1: icmp_seq=1 ttl=64 time=38.181 ms --- 10.1.0.1 ping statistics --- 2 packets transmitted, 2 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 37.025/37.603/38.181/0.578 ms也可以使用 Tailscale CLI 来测试:$ /Applications/Tailscale.app/Contents/MacOS/Tailscale ping 10.1.0.1 pong from coredns (10.1.0.1) via xxxx:41641 in 36ms如果你没有美区 ID,无法安装 App,可以直接使用命令行版本,通过 Homebrew 安装即可:$ brew install tailscaleAndroidAndroid 客户端就比较麻烦了,需要自己修改源代码编译 App,具体可参考这个 issue[9]。编译过程还是比较麻烦的,需要先修改源码,然后构建一个包含编译环境的 Docker 镜像,最后再通过该镜像启动容器编译 apk。我知道很多人一看麻烦就不想搞了,这个问题不大,我送佛送到西,提供了一条龙服务,你只需 fork 我的 GitHub 仓库 tailscale-android[10]:然后在你的仓库中点击 Settings 标签,找到 Secrets 下拉框中的 Actions 选项:选择 New repository secret 添加一个 secret 叫 HEADSCALE_URL,将你的 Headscale 服务公网地址填入其中:添加在这里的配置,将只对你可见,不用担心会泄露给他人。然后点击 Actions 标签,选择 Release Workflow。你会看到一个 Run workflow 按钮,点击它,然后在下拉框中点击 Run workflow。流水线就会开始执行,执行成功后就会在 Release 页面看到编译好的 apk。接下来的事情就简单了,下载这个 apk 到你的 Android 手机上安装就好了。安装完成后打开 Tailscale App,选择 Sign in with other。然后就会跳出这个页面:将其中的命令粘贴到 Headscale 所在主机的终端,将 NAMESPACE 替换为之前创建的 namespace,然后执行命令即可。注册成功后可将该页面关闭,回到 App 主页,效果如图:回到之前的 GitHub 仓库,刚才我们是通过手动触发 Workflow 来编译 apk 的,有没有办法自动编译呢?只要 Tailscale 官方仓库有更新,就立即触发 Workflow 开始编译。那当然是可以实现的,而且我已经实现了,仔细看 GitHub Actions 的编排文件:红框圈出来的部分表示只要仓库的 main 分支有更新,便会触发 Workflow。现在的问题是如何让 main 分支和上游官方仓库一致,一直保持在最新状态。这个问题使用第三方 Github App 就可以解决,这个 App 名字简单粗暴,就叫 Pull[11],它的作用非也很简单粗暴:保持你的 Fork 在最新状态。Pull 的使用方法很简单:打开 Pull App[12] 页面点击右上角绿色的 install 按钮在选项页面,使用默认的 All repositories 即可(你也可以选择指定的仓库,比如 tailscale-android),然后点击绿色的 install 按钮:简单三步,Pull App 就安装好了。接下来 Pull App 会每天定时帮你更新代码库,使你 fork 的代码始终是最新版的。WindowsWindows Tailscale 客户端想要使用 Headscale 作为控制服务器,只需在浏览器中打开 URL:http://<HEADSCALE_PUB_IP>:8080/windows,便会出现如下的界面:按照其中的步骤操作即可。其他 Linux 发行版除了常规的 Linux 发行版之外,还有一些特殊场景的 Linux 发行版,比如 OpenWrt、威联通(QNAP)、群晖等,这些发行版的安装方法已经有人写好了,这里就不详细描述了,我只给出相关的 GitHub 仓库,大家如果自己有需求,直接去看相关仓库的文档即可。OpenWrt:https://github.com/adyanth/openwrt-tailscale-enabler群晖:https://github.com/tailscale/tailscale-synology威联通:https://github.com/ivokub/tailscale-qpkgiOSTailscale iOS 客户端源代码没有开源,目前还无法破解使其使用第三方控制服务器,遗憾~~打通局域网到目前为止我们只是打造了一个点对点的 Mesh 网络,各个节点之间都可以通过 WireGuard 的私有网络 IP 进行直连。但我们可以更大胆一点,还记得我在文章开头提到的访问家庭内网的资源吗?我们可以通过适当的配置让每个节点都能访问其他节点的局域网 IP。这个使用场景就比较多了,你可以直接访问家庭内网的 NAS,或者内网的任何一个服务,更高级的玩家可以使用这个方法来访问云上 Kubernetes 集群的 Pod IP 和 Service IP。假设你的家庭内网有一台 Linux 主机(比如 OpenWrt)安装了 Tailscale 客户端,我们希望其他 Tailscale 客户端可以直接通过家中的局域网 IP(例如 192.168.100.0/24) 访问家庭内网的任何一台设备。配置方法很简单,首先需要设置 IPv4 与 IPv6 路由转发:$ echo 'net.ipv4.ip_forward = 1' | tee /etc/sysctl.d/ipforwarding.conf $ echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/ipforwarding.conf $ sysctl -p /etc/sysctl.d/ipforwarding.conf客户端修改注册节点的命令,在原来命令的基础上加上参数 --advertise-routes=192.168.100.0/24。$ tailscale up --login-server=http://<HEADSCALE_PUB_IP>:8080 --accept-routes=true --accept-dns=false --advertise-routes=192.168.100.0/24在 Headscale 端查看路由,可以看到相关路由是关闭的。$ headscale nodes list|grep openwrt 6 | openwrt | [7LdVc] | default | 10.1.0.6 | false | 2022-03-20 15:50:46 | online | no $ headscale routes list -i 6 Route | Enabled 192.168.100.0/24 | false开启路由:$ headscale routes enable -i 6 -r "192.168.100.0/24" Route | Enabled 192.168.100.0/24 | true其他节点查看路由结果:$ ip route show table 52|grep "192.168.100.0/24" 192.168.100.0/24 dev tailscale0现在你在任何一个 Tailscale 客户端所在的节点都可以 ping 通家庭内网的机器了,你在公司或者星巴克也可以像在家里一样用同样的 IP 随意访问家中的任何一个设备,就问你香不香?总结目前从稳定性来看,Tailscale 比 Netmaker 略胜一筹,基本上不会像 Netmaker 一样时不时出现 ping 不通的情况,这取决于 Tailscale 在用户态对 NAT 穿透所做的种种优化,他们还专门写了一篇文章介绍 NAT 穿透的原理[13],中文版[14]由国内的 eBPF 大佬赵亚楠翻译,墙裂推荐大家阅读。放一张图给大家感受一下:本文给大家介绍了 Tailscale 和 Headscale,包括 Headscale 的安装部署和各个平台客户端的接入,以及如何打通各个节点所在的局域网。下篇文章将会给大家介绍如何让 Tailscale 使用自定义的 DERP Servers(也就是中继服务器)。原文链接🔗 https://fuckcloudnative.io/posts/custom-derp-servers/👉上面我们介绍了如何使用 Headscale 替代 Tailscale 官方的控制服务器,并接入各个平台的客户端。本文将会介绍如何让 Tailscale 使用自定义的 DERP Servers。可能很多人都不知道 DERP 是个啥玩意儿,没关系,我先从中继服务器开始讲起。STUN 是什么Tailscale 的终极目标是让两台处于网络上的任何位置的机器建立点对点连接(直连),但现实世界是复杂的,大部份情况下机器都位于 NAT 和防火墙后面,这时候就需要通过打洞来实现直连,也就是 NAT 穿透。NAT 按照 NAT 映射行为和有状态防火墙行为可以分为多种类型,但对于 NAT 穿透来说根本不需要关心这么多类型,只需要看 NAT 或者有状态防火墙是否会严格检查目标 Endpoint,根据这个因素,可以将 NAT 分为 Easy NAT 和 Hard NAT。Easy NAT 及其变种称为 “Endpoint-Independent Mapping” (EIM,终点无关的映射) 这里的 Endpoint 指的是目标 Endpoint,也就是说,有状态防火墙只要看到有客户端自己发起的出向包,就会允许相应的入向包进入,不管这个入向包是谁发进来的都可以。hard NAT 以及变种称为 “Endpoint-Dependent Mapping”(EDM,终点相关的映射) 这种 NAT 会针对每个目标 Endpoint 来生成一条相应的映射关系。在这样的设备上,如果客户端向某个目标 Endpoint 发起了出向包,假设客户端的公网 IP 是 2.2.2.2,那么有状态防火墙就会打开一个端口,假设是 4242。那么只有来自该目标 Endpoint 的入向包才允许通过 2.2.2.2:4242,其他客户端一律不允许。这种 NAT 更加严格,所以叫 Hard NAT。对于 Easy NAT,我们只需要提供一个第三方的服务,它能够告诉客户端“它看到的客户端的公网 ip:port 是什么”,然后将这个信息以某种方式告诉通信对端(peer),后者就知道该和哪个地址建连了!这种服务就叫 STUN (Session Traversal Utilities for NAT,NAT会话穿越应用程序)。它的工作流程如下图所示:笔记本向 STUN 服务器发送一个请求:“从你的角度看,我的地址什么?”STUN 服务器返回一个响应:“我看到你的 UDP 包是从这个地址来的:ip:port”。中继是什么对于 Hard NAT 来说,STUN 就不好使了,即使 STUN 拿到了客户端的公网 ip:port 告诉通信对端也于事无补,因为防火墙是和 STUN 通信才打开的缺口,这个缺口只允许 STUN 的入向包进入,其他通信对端知道了这个缺口也进不来。通常企业级 NAT 都属于 Hard NAT。这种情况下打洞是不可能了,但也不能就此放弃,可以选择一种折衷的方式:创建一个中继服务器(relay server),客户端与中继服务器进行通信,中继服务器再将包中继(relay)给通信对端。至于中继的性能,那要看具体情况了:如果能直连,那显然没必要用中继方式;但如果无法直连,而中继路径又非常接近双方直连的真实路径,并且带宽足够大,那中继方式并不会明显降低通信质量。延迟肯定会增加一点,带宽会占用一些,但相比完全连接不上,还是可以接受的。事实上对于大部分网络而言,Tailscale 都可以通过各种黑科技打洞成功,只有极少数情况下才会选择中继,中继只是一种 fallback 机制。中继协议简介中继协议有多种实现方式。TURNTURN 即 Traversal Using Relays around NAT,这是一种经典的中继实现方式,核心理念是:用户(人)先去公网上的 TURN 服务器认证,成功后后者会告诉你:“我已经为你分配了 ip:port,接下来将为你中继流量”,然后将这个 ip:port 地址告诉对方,让它去连接这个地址,接下去就是非常简单的客户端/服务器通信模型了。与 STUN 不同,这种协议没有真正的交互性,不是很好用,因此 Tailscale 并没有采用 TURN 作为中继协议。DERPDERP 即 Detoured Encrypted Routing Protocol,这是 Tailscale 自研的一个协议:它是一个通用目的包中继协议,运行在 HTTP 之上,而大部分网络都是允许 HTTP 通信的。它根据目的公钥(destination’s public key)来中继加密的流量(encrypted payloads)。Tailscale 会自动选择离目标节点最近的 DERP server 来中继流量Tailscale 使用的算法很有趣,所有客户端之间的连接都是先选择 DERP 模式(中继模式),这意味着连接立即就能建立(优先级最低但 100% 能成功的模式),用户不用任何等待。然后开始并行地进行路径发现,通常几秒钟之后,我们就能发现一条更优路径,然后将现有连接透明升级(upgrade)过去,变成点对点连接(直连)。因此,DERP 既是 Tailscale 在 NAT 穿透失败时的保底通信方式(此时的角色与 TURN 类似),也是在其他一些场景下帮助我们完成 NAT 穿透的旁路信道。换句话说,它既是我们的保底方式,也是有更好的穿透链路时,帮助我们进行连接升级(upgrade to a peer-to-peer connection)的基础设施。自建私有 DERP serverTailscale 的私钥只会保存在当前节点,因此 DERP server 无法解密流量,它只能和互联网上的其他路由器一样,呆呆地将加密的流量从一个节点转发到另一个节点,只不过 DERP 使用了一个稍微高级一点的协议来防止滥用。Tailscale 开源了 DERP 服务器的代码,如果你感兴趣,可以阅读 DERP 的源代码[1]。Tailscale 官方内置了很多 DERP 服务器,分步在全球各地,惟独不包含中国大陆,原因你懂得。这就导致了一旦流量通过 DERP 服务器进行中继,延时就会非常高。而且官方提供的 DERP 服务器是万人骑,存在安全隐患。为了实现低延迟、高安全性,我们可以参考 Tailscale 官方文档[2]自建私有的 DERP 服务器。有两种部署模式,一种是基于域名,另外一种不需要域名,可以直接使用 IP,不过需要一点黑科技。我们先来看最简单的使用域名的方案。使用域名这种方案需要满足以下几个条件:要有自己的域名,并且申请了 SSL 证书需要准备一台或多台云主机如果服务器在国内,域名需要备案如果服务器在国外,则不需要备案如果以上条件都俱备,就可以按照下面的步骤开始部署了。推荐直接使用 Docker 来部署,我已经构建好了 Docker 镜像,直接部署就可以了:🐳 → docker run --restart always \ --name derper -p 12345:12345 -p 3478:3478/udp \ -v /root/.acme.sh/xxxx/:/app/certs \ -e DERP_CERT_MODE=manual \ -e DERP_ADDR=:12345 \ -e DERP_DOMAIN=xxxx \ -d ghcr.io/yangchuansheng/derper:latest有几点需要注意:能用 443 端口尽量用 443 端口,实在不行再用别的端口;默认情况下也会开启 STUN 服务,UDP 端口是 3478;防火墙需要放行端口 12345 和 3478;准备好 SSL 证书;域名部分我打了码,请换成你自己的域名。关于证书部分需要重点说明:假设你的域名是 xxx.com,那么证书的名称必须是 xxx.com.crt,一个字符都不能错!同理,私钥名称必须是 xxx.com.key,一个字符都不能错!查看容器日志:🐳 → docker logs -f derper 2022/03/26 11:36:28 no config path specified; using /var/lib/derper/derper.key 2022/03/26 11:36:28 derper: serving on :12345 with TLS 2022/03/26 11:36:28 running STUN server on [::]:3478目前 derper 运行一段时间就会崩溃,暂时还没有更好的解决方案,只能通过定时重启来解决,比如通过 crontab 来设置每两小时重启一次容器:0 */2 * * * docker restart derper &> /dev/null具体可参考这个 issue:Derper TLS handshake error: remote error: tls: internal error[3]部署好 derper 之后,就可以修改 Headscale 的配置来使用自定义的 DERP 服务器了。Headscale 可以通过两种形式的配置来使用自定义 DERP:一种是在线 URL,格式是 JSON,与 Tailscale 官方控制服务器使用的格式和语法相同。另一种是本地文件,格式是 YAML。我们可以直接使用本地的 YAML 配置文件,内容如下:# /etc/headscale/derp.yaml regions: 900: regionid: 900 regioncode: thk regionname: Tencent Hongkong nodes: - name: 900a regionid: 900 hostname: xxxx ipv4: xxxx stunport: 3478 stunonly: false derpport: 12345 - name: 900b regionid: 900 hostname: xxxx ipv4: xxxx stunport: 3478 stunonly: false derpport: 12345 901: regionid: 901 regioncode: hs regionname: Huawei Shanghai nodes: - name: 901a regionid: 901 hostname: xxxx ipv4: xxxx stunport: 3478 stunonly: false derpport: 12345配置说明:regions 是 YAML 中的对象,下面的每一个对象表示一个可用区,每个可用区里面可设置多个 DERP 节点,即 nodes。每个可用区的 regionid 不能重复。每个 node 的 name 不能重复。regionname 一般用来描述可用区,regioncode 一般设置成可用区的缩写。ipv4 字段不是必须的,如果你的域名可以通过公网解析到你的 DERP 服务器地址,这里可以不填。如果你使用了一个二级域名,而这个域名你并没有在公共 DNS server 中添加相关的解析记录,那么这里就需要指定 IP(前提是你的证书包含了这个二级域名,这个很好支持,搞个泛域名证书就行了)。stunonly: false 表示除了使用 STUN 服务,还可以使用 DERP 服务。上面的配置中域名和 IP 部分我都打码了,你需要根据你的实际情况填写。接下来还需要修改 Headscale 的配置文件,引用上面的自定义 DERP 配置文件。需要修改的配置项如下:# /etc/headscale/config.yaml derp: # List of externally available DERP maps encoded in JSON urls: # - https://controlplane.tailscale.com/derpmap/default # Locally available DERP map files encoded in YAML # # This option is mostly interesting for people hosting # their own DERP servers: # https://tailscale.com/kb/1118/custom-derp-servers/ # # paths: # - /etc/headscale/derp-example.yaml paths: - /etc/headscale/derp.yaml # If enabled, a worker will be set up to periodically # refresh the given sources and update the derpmap # will be set up. auto_update_enabled: true # How often should we check for DERP updates? update_frequency: 24h可以把 Tailscale 官方的 DERP 服务器禁用,来测试自建的 DERP 服务器是否能正常工作。修改完配置后,重启 headscale 服务:$ systemctl restart headscale在 Tailscale 客户端上使用以下命令查看目前可以使用的 DERP 服务器:$ tailscale netcheck Report: * UDP: true * IPv4: yes, xxxxx:57068 * IPv6: no * MappingVariesByDestIP: false * HairPinning: false * PortMapping: * Nearest DERP: Tencent Hongkong * DERP latency: - thk: 39.7ms (Tencent Hongkong)tailscale netcheck 实际上只检测 3478/udp 的端口, 就算 netcheck 显示能连,也不一定代表 12345 端口可以转发流量。最简单的办法是直接打开 DERP 服务器的 URL:https://xxxx:12345,如果看到如下页面,且地址栏的 SSL 证书标签显示正常可用,那才是真没问题了。查看与通信对端的连接方式:$ tailscale status 10.1.0.5 coredns default linux - carsondemacbook-pro default macOS active; direct xxxx:2756; offline, tx 50424 rx 34056 oneplus-8t default android active; relay "thk"; offline, tx 1608 rx 1552 openwrt default linux active; direct xxxx:2834; offline, tx 1403688 rx 1217620这个客户端是一台云主机,有 3 个通信对端,分别是 macOS、OpenWRT 与 Android 手机,macOS 和 OpenWRT 都处于电信家庭内网中,Android 手机使用的是电信流量。可以看到只有 Android 手机是通过自定义的 DERP 服务器来中继流量的,打洞成功率相当高。使用 ping 来测试连通性:$ ping 10.1.0.8 PING 10.1.0.8 (10.1.0.8) 56(84) bytes of data. 64 bytes from 10.1.0.8: icmp_seq=1 ttl=64 time=150 ms 64 bytes from 10.1.0.8: icmp_seq=2 ttl=64 time=131 ms 64 bytes from 10.1.0.8: icmp_seq=3 ttl=64 time=161 ms 64 bytes from 10.1.0.8: icmp_seq=4 ttl=64 time=137 ms 64 bytes from 10.1.0.8: icmp_seq=5 ttl=64 time=156 ms 64 bytes from 10.1.0.8: icmp_seq=6 ttl=64 time=169 ms ^C --- 10.1.0.8 ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5005ms rtt min/avg/max/mdev = 131.728/151.154/169.627/13.193 ms也可以使用 Tailscale 命令行工具来测试:$ tailscale ping 10.1.0.8 pong from oneplus-8t (10.1.0.8) via DERP(thk) in 104ms pong from oneplus-8t (10.1.0.8) via DERP(thk) in 111ms pong from oneplus-8t (10.1.0.8) via DERP(thk) in 105ms这个更加友好一点,会直接告诉你是通过 DERP 中继服务器来和对方通信的。如果当前 Tailscale 客户端所在主机开启了 IPv6,那么与手机便可以直接通过 IPv6 点对点连接:$ /Applications/Tailscale.app/Contents/MacOS/Tailscale status coredns default linux active; direct xxxx:45986; offline, tx 124352 rx 185736 oneplus-8t default android active; direct [240e:472:da0:24a2:a07f:2a67:2a1e:4475]:37237; offline, tx 125216 rx 20052 openwrt default linux active; direct [240e:390:caf:1870:c02c:e8ff:feb9:b0b]:41641; offline, tx 181992 rx 3910120 $ /Applications/Tailscale.app/Contents/MacOS/Tailscale ping 10.1.0.8 pong from oneplus-8t (10.1.0.8) via [240e:472:da0:24a2:a07f:2a67:2a1e:4475]:37237 in 62ms所以如果你开启了 IPv6,可以大大增加点对点连接的成功率。使用纯 IP我知道,大部分人是没有自己的域名的。再退一步,就算有自己的域名,如果没有备案,也是没办法部署在国内服务器上使用的。这个时候我们就只能从 derper 源码上动手脚了,找到 tailscale 仓库中的 cmd/derper/cert.go 文件,将与域名验证相关的内容删除或注释:func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { //if hi.ServerName != m.hostname { // return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName) //} return m.cert, nil }还需要创建自签名证书,可以通过脚本来创建:# build_cert.sh #!/bin/bash CERT_HOST=$1 CERT_DIR=$2 CONF_FILE=$3 echo "[req] default_bits = 2048 distinguished_name = req_distinguished_name req_extensions = req_ext x509_extensions = v3_req prompt = no [req_distinguished_name] countryName = XX stateOrProvinceName = N/A localityName = N/A organizationName = Self-signed certificate commonName = $CERT_HOST: Self-signed certificate [req_ext] subjectAltName = @alt_names [v3_req] subjectAltName = @alt_names [alt_names] IP.1 = $CERT_HOST " > "$CONF_FILE" mkdir -p "$CERT_DIR" openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout "$CERT_DIR/$CERT_HOST.key" -out "$CERT_DIR/$CERT_HOST.crt" -config "$CONF_FILE"重新编写 Dockerfile,将 derper 的域名设置为 127.0.0.1:FROM golang:latest AS builder WORKDIR /app # ========= CONFIG ========= # - download links ENV MODIFIED_DERPER_GIT=https://github.com/yangchuansheng/ip_derper.git ENV BRANCH=ip_derper # ========================== # build modified derper RUN git clone -b $BRANCH $MODIFIED_DERPER_GIT tailscale --depth 1 && \ cd /app/tailscale/cmd/derper && \ /usr/local/go/bin/go build -ldflags "-s -w" -o /app/derper && \ cd /app && \ rm -rf /app/tailscale FROM ubuntu:20.04 WORKDIR /app # ========= CONFIG ========= # - derper args ENV DERP_HOST=127.0.0.1 ENV DERP_CERTS=/app/certs/ ENV DERP_STUN true ENV DERP_VERIFY_CLIENTS false # ========================== # apt RUN apt-get update && \ apt-get install -y openssl curl COPY build_cert.sh /app/ COPY --from=builder /app/derper /app/derper # build self-signed certs && start derper CMD bash /app/build_cert.sh $DERP_HOST $DERP_CERTS /app/san.conf && \ /app/derper --hostname=$DERP_HOST \ --certmode=manual \ --certdir=$DERP_CERTS \ --stun=$DERP_STUN \ --verify-clients=$DERP_VERIFY_CLIENTS构建好镜像后,就可以在你想部署 derper 的主机上直接通过该镜像启动 derper 容器了,命令如下:🐳 → docker run --restart always --net host --name derper -d ghcr.io/yangchuansheng/ip_derper和使用域名的方案一样,防火墙需要放行相应端口(12345 与 3478)。查看容器日志:🐳 → docker logs -f derper Generating a RSA private key .......................................+++++ ..............+++++ writing new private key to '/app/certs//127.0.0.1.key' ----- 2022/03/26 14:30:31 no config path specified; using /var/lib/derper/derper.key 2022/03/26 14:30:31 derper: serving on :443 with TLS 2022/03/26 14:30:31 running STUN server on [::]:3478如果你想自己构建 derper 镜像,可以参考我的 GitHub 仓库[4]。下面就是骚操作了,我们在 Headscale 的配置中需要将 DERP 的域名设置为 IP!不理解的可以再消化一下,然后继续往下看哈哈~~除了 derper 之外,Tailscale 客户端还需要跳过域名验证,这个需要在 DERP 的配置中设置。而 Headscale 的本地 YAML 文件目前还不支持这个配置项,所以没办法,咱只能使用在线 URL 了。JSON 配置内容如下:{ "Regions": { "901": { "RegionID": 901, "RegionCode": "ali-sh", "RegionName": "Aliyun Shanghai", "Nodes": [ { "Name": "901a", "RegionID": 901, "DERPPort": 443, "HostName": "xxxx", "IPv4": "xxxx", "InsecureForTests": true } ] } } }配置解析:HostName 直接填 derper 的公网 IP,即和 IPv4 的值相同。InsecureForTests 一定要设置为 true,以跳过域名验证。你需要把这个 JSON 文件变成 Headscale 服务器可以访问的 URL,比如在 Headscale 主机上搭个 Nginx,或者上传到对象存储(比如阿里云 OSS)。接下来还需要修改 Headscale 的配置文件,引用上面的自定义 DERP 的 URL。需要修改的配置项如下:# /etc/headscale/config.yaml derp: # List of externally available DERP maps encoded in JSON urls: # - https://controlplane.tailscale.com/derpmap/default - https://xxxxx/derp.json # Locally available DERP map files encoded in YAML # # This option is mostly interesting for people hosting # their own DERP servers: # https://tailscale.com/kb/1118/custom-derp-servers/ # # paths: # - /etc/headscale/derp-example.yaml paths: - /etc/headscale/derp.yaml # If enabled, a worker will be set up to periodically # refresh the given sources and update the derpmap # will be set up. auto_update_enabled: true # How often should we check for DERP updates? update_frequency: 24h修改完配置后,重启 headscale 服务:$ systemctl restart headscale在 Tailscale 客户端上使用以下命令查看目前可以使用的 DERP 服务器:$ tailscale netcheck Report: * UDP: true * IPv4: yes, 192.168.100.1:49656 * IPv6: no * MappingVariesByDestIP: true * HairPinning: false * PortMapping: UPnP * Nearest DERP: Home Hangzhou * DERP latency: - home: 9.7ms (Home Hangzhou) - hs: 25.2ms (Huawei Shanghai) - thk: 43.5ms (Tencent Hongkong)再次查看与通信对端的连接方式:$ tailscale status coredns default linux active; direct xxxx:45986; offline, tx 131012 rx 196020 oneplus-8t default android active; relay "home"; offline, tx 211900 rx 22780 openwrt default linux active; direct 192.168.100.254:41641; offline, tx 189868 rx 4074772可以看到这一次 Tailscale 自动选择了一个线路最优的国内的 DERP 服务器作为中继,可以测试一下延迟:$ tailscale ping 10.1.0.8 pong from oneplus-8t (10.1.0.8) via DERP(home) in 30ms pong from oneplus-8t (10.1.0.8) via DERP(home) in 45ms pong from oneplus-8t (10.1.0.8) via DERP(home) in 30ms完美!这里的 home 当然是我的家庭宽带,部署方式与上面所说的国内云主机类似,你需要额外开启公网的端口映射(12345/tcp, 3478/udp)。还有一点需要注意的是配置内容:{ "Regions": { "901": { "RegionID": 901, "RegionCode": "ali-sh", "RegionName": "Aliyun Shanghai", "Nodes": [ { "Name": "901a", "RegionID": 901, "DERPPort": 443, "HostName": "xxxx", "IPv4": "xxxx", "InsecureForTests": true } ] }, "902": { "RegionID": 902, "RegionCode": "home", "RegionName": "Home Hangzhou", "Nodes": [ { "Name": "902a", "RegionID": 902, "DERPPort": 12345, "HostName": "xxxx", "InsecureForTests": true } ] } } }与国内云主机相比,家庭宽带的配置有两点不同:需要删除 IPv4 配置项。因为家用宽带的公网 IP 是动态变化的,所以你需要使用 DDNS 来动态解析公网 IP。HostName 最好填域名,因为你的公网 IP 是动态变化的,没法填写 IP,除非你不停地修改配置文件。填域名也没关系啦,反正不会验证域名的,也不用关心证书的事情,只要域名能解析到你的公网 IP 即可。防止 DERP 被白嫖默认情况下 DERP 服务器是可以被白嫖的,只要别人知道了你的 DERP 服务器的地址和端口,就可以为他所用。如果你的服务器是个小水管,用的人多了可能会把你撑爆,因此我们需要修改配置来防止被白嫖。特别声明:只有使用域名的方式才可以通过认证防止被白嫖,使用纯 IP 的方式无法防白嫖,你只能小心翼翼地隐藏好你的 IP 和端口,不能让别人知道。只需要做两件事情:1、在 DERP 服务器上安装 Tailscale。第一步需要在 DERP 服务所在的主机上安装 Tailscale 客户端,启动 tailscaled 进程。2、derper 启动时加上参数 --verify-clients。本文推荐的是通过容器启动,Dockerfile 内容[5]如下:FROM golang:latest AS builder LABEL org.opencontainers.image.source https://github.com/yangchuansheng/docker-image WORKDIR /app # https://tailscale.com/kb/1118/custom-derp-servers/ RUN go install tailscale.com/cmd/derper@main FROM ubuntu WORKDIR /app ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends apt-utils && \ apt-get install -y ca-certificates && \ mkdir /app/certs ENV DERP_DOMAIN your-hostname.com ENV DERP_CERT_MODE letsencrypt ENV DERP_CERT_DIR /app/certs ENV DERP_ADDR :443 ENV DERP_STUN true ENV DERP_HTTP_PORT 80 ENV DERP_VERIFY_CLIENTS false COPY --from=builder /go/bin/derper . CMD /app/derper --hostname=$DERP_DOMAIN \ --certmode=$DERP_CERT_MODE \ --certdir=$DERP_CERT_DIR \ --a=$DERP_ADDR \ --stun=$DERP_STUN \ --http-port=$DERP_HTTP_PORT \ --verify-clients=$DERP_VERIFY_CLIENTS默认情况下 --verify-clients 参数设置的是 false。我们不需要对 Dockerfile 内容做任何改动,只需在容器启动时加上环境变量即可,将之前的启动命令修改一下:🐳 → docker run --restart always \ --name derper -p 12345:12345 -p 3478:3478/udp \ -v /root/.acme.sh/xxxx/:/app/certs \ -e DERP_CERT_MODE=manual \ -e DERP_ADDR=:12345 \ -e DERP_DOMAIN=xxxx \ -e DERP_VERIFY_CLIENTS=true \ -d ghcr.io/yangchuansheng/derper:latest这样就大功告成了,别人即使知道了你的 DERP 服务器地址也无法使用,但还是要说明一点,即便如此,你也应该尽量不让别人知道你的服务器地址,防止别人有可趁之机。总结本文给大家介绍了 STUN 对于辅助 NAT 穿透的意义,科普了几种常见的中继协议,包含 Tailscale 自研的 DERP 协议。最后手把手教大家如何自建私有的 DERP 服务器,并让 Tailscale 使用我们自建的 DERP 服务器。参考资料NAT 穿透是如何工作的:技术原理及企业级实践[6]Custom DERP Servers[7]Encrypted TCP relays (DERP)[8]引用链接全互联模式(full mesh): https://fuckcloudnative.io/posts/wireguard-full-mesh/#1-全互联模式架构与配置Netmaker: https://fuckcloudnative.io/posts/configure-a-mesh-network-with-netmaker/GitHub 仓库: https://github.com/tailscale/Headscale: https://github.com/juanfont/headscaleWhat is a tailnet: https://tailscale.com/kb/1136/tailnet/Windows 客户端文档: https://github.com/juanfont/headscale/blob/main/docs/windows-client.md需要自己编译客户端: https://github.com/juanfont/headscale/issues/58#issuecomment-950386833静态编译的二进制文件: https://tailscale.com/download/linux/static这个 issue: https://github.com/juanfont/headscale/issues/58#issuecomment-950386833tailscale-android: https://github.com/yangchuansheng/tailscale-androidPull: https://github.com/apps/pullPull App: https://github.com/apps/pullNAT 穿透的原理: https://tailscale.com/blog/how-nat-traversal-works/中文版: https://arthurchiao.art/blog/how-nat-traversal-works-zh/DERP 的源代码: https://github.com/tailscale/tailscale/tree/main/derpTailscale 官方文档: https://tailscale.com/kb/1118/custom-derp-servers/Derper TLS handshake error: remote error: tls: internal error: https://github.com/tailscale/tailscale/issues/4082我的 GitHub 仓库: https://github.com/yangchuansheng/ip_derperDockerfile 内容: https://github.com/yangchuansheng/docker-image/blob/master/derper/DockerfileNAT 穿透是如何工作的:技术原理及企业级实践: https://arthurchiao.art/blog/how-nat-traversal-works-zh/Custom DERP Servers: https://tailscale.com/kb/1118/custom-derp-servers/Encrypted TCP relays (DERP): https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp
2022年08月17日
575 阅读
2 评论
1 点赞
1
...
6
7
8
...
10
您的IP: