CI CD

代码 -> 构建 -> 集成 -> 测试 -> 发布 -> 部署

|———持续集成——| CI

|———–持续发布————| CD

|—————持续部署—————| CD

CI CD

  • 持续集成(CI)

  • 持续交付和持续部署(CD)

  • 现代软件开发的需求加上部署到不同基础设施的复杂性使得创建应用程序成为一个繁琐的过程。当应用程序出现规模性增长,开发团队人员变得更分散时,快速且不断地生产和发布软件的流程将会变得更加困难。

  • 为了解决这些问题,开发团队开始探索新的策略来使他们的构建、测试和发布流程自动化,以帮助其更快地部署新的生产。这就是持续交付和持续集成发展的由来。

流程

自动构建

在软件开发过程中,构建流程会将开发人员生成的代码转换为可执行的可用软件。

对于Go或者C语言等编译语言,此阶段需要通过编译器运行源代码以生成独立的二进制文件。

对于JavaScript或PHP等解释性语言,没有编译的步骤,但是代码依旧需要在特定的时间内冻结、绑定依赖项、打包以便于分发。这些过程通常称为“构建”或“发布”的工件。

虽然开发人员可以手动构建,但这样操作有诸多不利。首先,从主动开发到创建构建的转变中引入了上下文转换,使得开发人员不得不停止生产效率更高的工作来专注于构建过程。其次,每个开发人员都在制作自己的工件,这可能导致构建过程不一致。

为了解决这些顾虑,许多开发团队配置了自动构建流水线。这些系统监视源代码存储库,并在检测到更改时自动启动预配置的构建过程。这一配置无需牵涉过多的人力在其中并且确保了每个构建过程一致。

进程,线程,协程

程序执行

  • 写在硬盘上的静态程序文件
  • cpu从磁盘上找到程序文件
  • .exe 文件中包含操作cpu的指令
  • 把指令放到内存中
  • 在内存中的指令就可以叫做cpu的进程

进程

对于cpu来讲,有一个时间线的概念,时间线上的每一个点对应着一个操作指令

操作系统把时间线分割为不同的时间片,时间片是执行程序的小单位

按照 1,2,1,2的顺序,来回切换任务的分配,可以让cpu调度进程,看起来在同时执行多个操作

进程比较重,每个任务分配一个进程,频繁切换的时候损耗比较大,所以有了线程的概念

进程之间内存相互独立,进程之间相互通信需要内核转发(系统调用)。也就是进程之间的通信需要经过操作系统,损耗比较大

进程主要占据的内存代码(进程的实现代码),数据(需要处理的数据),文件(和硬盘文件关联的文件句柄,需要通过系统内核,来完成文件的操作)

执行的时候需要写寄存器,在进程切换的时候需要保存寄存器中信息的状态,把寄存器中的数据写到内存中。

栈 函数调用栈

线程

线程比较轻,线程依附与进程,是多个线程对应一个进程的关系,

线程共享进程的内存(代码,数据,文件),但是有自己独立的寄存器和栈,所以线程比较轻

线程共享数据存在的问题,可能读写同时操作,需要线程锁

IO 密集型

输入输出的速度远小于CPU的速度。

多任务 -> 多线程 -> 时间驱动 -> 协程

CPU密集型

多进程 -> 多线程

网络配置问题

可以使用type +命令来查看命令所在目录

查看网络配置基本信息

centenOS 6 之前使用 ifconfg 命令, 之后使用ip 命令, 也可以安装 ifconfig命令包

重启网卡

如果可以直接操作服务其 可以使用 ifup ,ifdown

如果提示系统没有命令可以使用 apt install ifupdown安装

如果是远程不能使用这个两个命令重启网卡,使用ifdown后会断开网络连接

可以在确保网络配置修改成功之后,使用 systemctl restar network

如果命令不可用可以使用 service network-manager restart

如果是 Kali Linux(Debian),则需要service networking restart

如果是Centos 8 需要nmcli c reload

查找占用端口

可以使用 ss 或 netstat 命令 后面加 -anp

如果没有 netstat 命令,可以通过 apt install net-tools 安装

最后使用 kill 命令杀掉端口

linux 常见问题

ps

查看进程

1
ps aux

kill pkill

1
kill -9 pid

pkill 后面可以直接写进程的名字

如果是一个服务使用 systemctl stop

w who

看谁正在连接系统

lsof 查看端口占用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#列出所有打开的文件:
lsof
备注: 如果不加任何参数,就会打开所有被打开的文件,建议加上一下参数来具体定位

# 查看谁正在使用某个文件
lsof /filepath/file

#递归查看某个目录的文件信息
lsof +D /filepath/filepath2/
备注: 使用了+D,对应目录下的所有子目录和文件都会被列出

# 比使用+D选项,遍历查看某个目录的所有文件信息 的方法
lsof | grep ‘/filepath/filepath2/’

# 列出某个用户打开的文件信息
lsof -u username
备注: -u 选项,u其实是user的缩写

# 列出某个程序所打开的文件信息
lsof -c mysql
备注: -c 选项将会列出所有以mysql开头的程序的文件,其实你也可以写成lsof | grep mysql,但是第一种方法明显比第二种方法要少打几个字符了

# 列出多个程序多打开的文件信息
lsof -c mysql -c apache

# 列出某个用户以及某个程序所打开的文件信息
lsof -u test -c mysql

# 列出除了某个用户外的被打开的文件信息
lsof -u ^root
备注:^这个符号在用户名之前,将会把是root用户打开的进程不让显示

# 通过某个进程号显示该进行打开的文件
lsof -p 1

# 列出多个进程号对应的文件信息
lsof -p 123,456,789

# 列出除了某个进程号,其他进程号所打开的文件信息
lsof -p ^1

# 列出所有的网络连接
lsof -i

# 列出所有tcp 网络连接信息
lsof -i tcp

# 列出所有udp网络连接信息
lsof -i udp

# 列出谁在使用某个端口
lsof -i :3306

# 列出谁在使用某个特定的udp端口
lsof -i udp:55

# 特定的tcp端口
lsof -i tcp:80

# 列出某个用户的所有活跃的网络端口
lsof -a -u test -i

# 列出所有网络文件系统
lsof -N

#域名socket文件
lsof -u

#某个用户组所打开的文件信息
lsof -g 5555

# 根据文件描述列出对应的文件信息
lsof -d description(like 2)

# 根据文件描述范围列出文件信息
lsof -d 2-3

SSH免密登录

  • 生成密钥对

    -t 指定密钥类型,默认是 rsa ,可以省略。
    -C 设置注释文字,比如邮箱。
    -f 指定密钥文件存储文件名。

    如果想免密登录,密钥不要填写密码,否则必须验证密钥密码才能登录

    1
    2
    3
    4
    ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

    # 或者创建使用 ed25519 加密算法的密钥
    ssh-keygen -t ed25519 -C "your_email@example.com"
  • 上传共钥到服务器对应账号的 home 目录下.ssh 文件夹下面,公钥的权限为 600

    linux 执行以下命令

    1
    ssh-copy-id -i mykey_rsa.pub you_user_name@xxx.xxx.xxx.xxx

    window 需要手动在服务器上创建文件,

    1
    2
    3
    4
    5
    6
    7
    touch  ~/.ssh/authorized_keys

    # 或直接写入文件
    echo "your_public_key_here" >> ~/.ssh/authorized_keys

    # 修改权限
    chmod 600 authorized_keys
  • 指定私钥登陆,私钥的权限为 600

    1
    ssh -i 私钥 user@xxx.xxx.xxx.xxx
  • 通过配置文件免密登陆,在本地服务器把私钥复制到 home 下的.ssh 文件夹下面

    创建名称为 config 的文件,配置单一服务器免密登陆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Host tencent
    HostName 124.222.139.87
    User ubuntu
    IdentityFile ./.ssh/id_ed25519
    IdentitiesOnly yes


    # Host 别名
    #  HostName IP
    #  Port 端口
    #  User 用户名
    #  IdentitiesOnly yes
    # IdentityFile ~/.ssh/user_rsa (私钥路径)
    # Protocal 2 (协议版本号)
    # Compression yes
    # ServerAliveInterval 60 (防止被踢配置,长时间没有操作会被踢掉,每隔60秒发一个信号)
    # ServerAliveCountMax 20 (最大连接数)
    # LogLevel INFO

    通配符配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Host app-produce
    HostName 192.168.1.10
    Port 22
    User appuser
    IdentityFile ~/.ssh/id_ed25519

    Host \*\_produce
    User commonuser
    IdentityFile ~/.ssh/id_ed25519
    Port 22

wget

wget 是一个从网络上自动下载文件的自由工具,支持通过 HTTP、HTTPS、FTP 三个最常见的 TCP/IP 协议 下载,并可以使用 HTTP 代理。”wget” 这个名称来源于 “World Wide Web” 与 “get” 的结合。

wget 可以在用户退出系统的之后在后台执行。这意味这你可以登录系统,启动一个 wget 下载任务,然后退出系统,wget 将在后台执行直到任务完成,相对于其它大部分浏览器在下载大量数据时需要用户一直的参与.

1
wget (选项) (参数)

其中选项如下:

-a<日志文件>:在指定的日志文件中记录资料的执行过程;

-A<后缀名>:指定要下载文件的后缀名,多个后缀名之间使用逗号进行分隔;

-b:进行后台的方式运行 wget;

-B<连接地址>:设置参考的连接地址的基地地址;

-c:继续执行上次终端的任务;

-C<标志>:设置服务器数据块功能标志 on 为激活,off 为关闭,默认值为 on;

-d:调试模式运行指令;

-D<域名列表>:设置顺着的域名列表,域名之间用“,”分隔;

-e<指令>:作为文件“.wgetrc”中的一部分执行指定的指令;

-h:显示指令帮助信息;

-i<文件>:从指定文件获取要下载的 URL 地址;

-l<目录列表>:设置顺着的目录列表,多个目录用“,”分隔;

-L:仅顺着关联的连接;

-r:递归下载方式;

-nc:文件存在时,下载文件不覆盖原有文件;

-nv:下载时只显示更新和出错信息,不显示指令的详细执行过程;

-q:不显示指令执行过程;

-nh:不查询主机名称;

-v:显示详细执行过程;

-V:显示版本信息;

–passive-ftp:使用被动模式 PASV 连接 FTP 服务器;

–follow-ftp:从 HTML 文件中下载 FTP 连接文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
wget http://test.com/testfile.zip ->下载指定文件到当前文件夹
wget -O wordpress.zip http://test.com/download ->指定保存名字
wget --limit-rate=300k http://www.linuxde.net/testfile.zip ->限制下载速度
wget -c http://www.linuxde.net/testfile.zip ->断点续传
wget -b http://www.linuxde.net/testfile.zip ->后台下载

# 设置使用指定浏览器下载(伪装下载)
wget --user-agent="Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16" http://www.linuxde.net/testfile.zip

wget --spider url ->测试下载
wget --tries=40 URL ->设置重试次数为40
wget -i filelist.txt ->从filelist.txt获取下载地址

# 镜像网站
# --miror开户镜像下载。
# -p下载所有为了html页面显示正常的文件。
# --convert-links下载后,转换成本地的链接。
# -P ./LOCAL保存所有文件和目录到本地指定目录
wget --mirror -p --convert-links -P ./LOCAL URL

wget --reject=gif ur ->下载一个网站,但你不希望下载图片,可以使用这条命令
wget -o download.log URL ->把下载信息存入日志文件
wget -Q5m -i filelist.txt ->限制总下载文件大小
wget -r -A.pdf url ->下载指定格式文件

# FTP下载
wget ftp-url
wget --ftp-user=USERNAME --ftp-password=PASSWORD url

修改系统时间时区

最标准且永久生效的方法是使用系统自带的 timedatectl 工具。

1
2
3
4
5
6
7
sudo timedatectl set-timezone Asia/Shanghai

#强制开启网络时间同步 (NTP)
sudo timedatectl set-ntp true

#检查时间状态
timedatectl status

lvm-cache

为HDD添加SSD作为缓存盘,提高读写速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 擦除所有分区签名(极其危险,请确保 /dev/nvme0n1 就是目标盘)
wipefs -a /dev/nvme0n1
wipefs -a /dev/sdc

# 彻底销毁 GPT 分区表
sgdisk --zap-all /dev/nvme0n1
sgdisk --zap-all /dev/sdc

# 创建物理卷 (PV),将这两块干净的硬盘注册为 LVM 的底层物理存储节点。
pvcreate /dev/sdc /dev/nvme0n1

# 创建基础卷组 (VG) 和逻辑卷 (LV):关键:此时只用机械硬盘.为了确保你的基础数据 100% 落在机械硬盘上,我们先只用机械硬盘创建一个卷组,并把它的全部空间划给一个逻辑卷。
vgcreate vg_home_data /dev/sdc

# 将 vg_home_data 的所有空间分配给逻辑卷 lv_home_data
lvcreate -n lv_home_data -l 100%FREE vg_home_data

# 如果提示 WARNING: zfs_member signature detected on /dev/vg_home_data/lv_home_data at offset 401408. Wipe it? [y/n]:
# 可以通过管道符批量处理
yes | lvcreate -n lv_home_data -l 100%FREE vg_home_data

#将 NVMe 固态扩容进卷组:房子建好了,现在把你的固态硬盘加入到 vg_home_data 这个卷组里,作为后续的缓存备用。
vgextend vg_home_data /dev/nvme0n1

# 在固态硬盘上创建缓存池,并绑定到机械硬盘的 lv_home_data 上
lvcreate -y --type cache --cachemode writeback -l 95%FREE -n home_data_cache vg_home_data/lv_home_data /dev/nvme0n1

# 查看命中率与"脏数据"
# WriteHits (写命中): 写入时成功被 SSD 接管的次数。多线程下载时,这个数值会疯狂暴涨,代表固态硬盘正在疯狂抗压。
# DirtyBlocks (脏数据块): 这是 Writeback 模式特有的概念!意思是“目前还在 SSD 里,还没有来得及搬到机械硬盘里的数据量”。下载时它会堆积,系统闲置时,LVM 会在后台默默把它们顺序写入机械盘,这个数值就会慢慢清零。

watch -n 1 "lvs -o name,data_percent,cache_write_hits,cache_dirty_blocks vg_home_data/lv_home_data"

LXC 容器使用LVM存储

如何配置给 LXC 容器使用LVM存储, 首先为什么不能直接直通给 LXC?是因为 LXC 容器的底层机制决定的。即便你现在手里是一个纯纯的物理机械硬盘 /dev/sda,我依然不建议你直接把它直通给 LXC 容器。

如果是 虚拟机 (VM): 虚拟机有自己完整的“虚拟主板”。你可以直接把 /dev/sda 或者 /dev/vg_home_data/lv_home_data 作为一块“虚拟硬盘”挂载给它。虚拟机开机后,会认为自己插了一块新硬盘,并在虚拟机内部自己进行格式化。

如果是 LXC 容器: LXC 没有“主板”,它就是一个隔离的进程,直接和 PVE 宿主机共用同一个 Linux 内核。如果你强行把裸设备(/dev/sda 或逻辑卷)映射进容器(需要改复杂的 cgroup 权限,特别是无特权容器),很容易遇到权限报错。

对 LXC 容器而言,不管是物理的 /dev/sda,还是带缓存魔法的 lv_home_data,最规范、最不容易出 Bug 的用法,都是让 PVE 宿主机去格式化它,把它变成一个文件夹,然后把文件夹映射给容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 在 PVE 宿主机上格式化并挂载
# 首先,我们要给这个裸设备穿上文件系统(推荐 ext4),并让 PVE 把它挂载出来。

# 1. 格式化为 ext4 文件系统(只需执行一次)
mkfs.ext4 /dev/vg_home_data/lv_home_data

# 2. 在 PVE 宿主机上创建一个挂载点(比如叫 gamedata)
mkdir -p /mnt/gamedata

# 3. 挂载上去
mount /dev/vg_home_data/lv_home_data /mnt/gamedata


#为了让 PVE 每次重启后都能自动挂载,你需要把它写进开机启动文件里, 编辑 fstab 文件
nano /etc/fstab
# 在末尾添加
/dev/vg_home_data/lv_home_data /mnt/gamedata ext4 defaults 0 2

# 将目录映射进 LXC 容器
# 在 pve UI 界面通过 Mount Point 添加挂在点,或通过以下命令,假设你的 LXC 容器的 ID 是 101(请替换为你的实际 ID)

pct set 101 -mp0 /mnt/gamedata,mp=/downloads

VM虚拟机使用LVM存储,需要用到“块设备直通(Block Device Passthrough)

配置开启 iSCSI 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 在的终端执行以下命令,安装并进入 targetcli 交互式命令行:

apt update
apt install targetcli-fb -y
targetcli

# 把你的 LVM Cache 逻辑卷注册为一个名为 home_data 的块设备
cd /backstores/block
create name=home_data dev=/dev/vg_home_data/lv_home_data

# 创建 iSCSI 目标 (Target):创建一个对外的 iSCSI 节点名称(IQN)。这里以当前的年份月份命名:
cd /iscsi
create iqn.2026-07.local.pve:homedata

# 映射 LUN (逻辑单元):将刚才创建的底层存储块 gamedisk 绑定到这个 iSCSI 目标上:
cd iqn.2026-07.local.pve:homedata/tpg1/luns
create /backstores/block/home_data

# 为了数据安全,我们要告诉 PVE 只允许你的 Windows 电脑连接,拦截网络里其他乱七八糟的请求。这里要用到你刚才在 Windows 上复制的 IQN 名字
# 在windows 打开 iscsi initiator 在 configuration 标签里面 initiator name
cd ../acls
create 你的Windows_IQN

# 所有的配置现在都在内存中,我们需要将其保存到配置文件并退出工具:

cd /
saveconfig
exit

Q: 如果 targetcli-fb 用 block Block 模式,数据也能先进 pve 的内存么,还是说只有fileio 才能进pve的内存?
A: 如果使用 Block 模式,数据将【基本跳过】PVE 的系统内存(Page Cache),直接砸向底层的块设备。而 FileIO 默认会大量吃掉 PVE 的内存。

这也是为什么我们在前一步骤中,必须配合 dm-writecache (SSD) 来使用 Block 模式的原因。下面为你拆解这两种模式在内存利用上的根本差异:

FileIO:严重依赖内存的伪装者
当你在 PVE 中使用 fileio 创建 iSCSI 目标时,Linux 内核会把这个虚拟磁盘当成一个普通的本地文件(.raw 或 .img)来处理。

数据流向:Windows 发送数据 ➡️ PVE 内存(Page Cache 页缓存) ➡️ PVE 机械硬盘。

现象:你会发现就算没有 SSD,一开始拷文件也能跑到 280MB/s。但这是虚假的繁荣,因为数据其实全堆在 PVE 的内存里。等内存快塞满时,内核开始手忙脚乱地把内存数据往机械硬盘里倒腾,速度会瞬间断崖式下跌到 0,甚至卡死。

致命风险:如果此时 PVE 突然死机或停电,堆在内存里(还没写进机械盘)的脏数据瞬间灰飞烟灭。这对于 iSCSI 来说是毁灭性的,会导致 Windows 端的 NTFS 索引损坏,整个盘变成 RAW。

Block 模式:直达底层的直通车 (Direct I/O)
当你在 LIO (targetcli) 中使用 block (iblock) 模式时,它绕过了 Linux 的虚拟文件系统(VFS)和页缓存(Page Cache)。

数据流向:Windows 发送数据 ➡️ PVE 网卡缓冲(微秒级停留) ➡️ 直接下发 SCSI 指令到底层块设备。

现象:系统不再用大量的内存去给写入做缓冲。这意味着,如果底层是机械硬盘,你的写入速度会直接被机械硬盘的真实速度(比如 100MB/s)卡死,甚至因为 iSCSI 的小块随机写入,速度可能掉到只有 30MB/s。

这就是为什么我们需要 dm-writecache! 因为 Block 模式没有了内存做缓冲,我们就用物理的 SSD (dm-writecache) 来代替内存的作用。

Q:为什么windows写入pve是直接写入内存,但是拷贝一个大电影的时候,复制速度会卡到0
A: PVE 的 LVM Cache 底层使用的是 dm-cache 技术,默认采用了极其聪明的 smq (Stochastic Multiqueue) 缓存策略。

这个策略有一个核心逻辑:保护 SSD 寿命,防止“缓存污染(Cache Thrashing)”。

当 Windows 刚开始传数据时,LVM 以为是普通的小文件写入,于是把数据丢进 SSD(这就是你看到缓存增长的原因)。

但是,当 LVM 发现这是一个连续的、巨大的数据流(比如你的 8GB 电影)时,它会立刻触发 sequential_threshold(顺序 I/O 阈值)保护机制。

LVM 的逻辑是:“这种连续大文件写进去就是占地方,以后大概率也是连续读,直接交给机械硬盘处理最合适,没必要磨损 SSD 的颗粒。”

结果:LVM 会在中途主动将后续的数据流从 SSD 切换,直接穿透到后端的机械硬盘上。因为你的单块机械硬盘真实写入速度就是 100MB/s 左右,所以最终速度被死死卡在这个数值。

Q: 开启 Turn off Windows write-cache buffer flushing on the device 有什么效果。
A: 无论你是否勾选这个选项,数据都会先进入 Windows 的内存(RAM)。

这里需要区分两个极其关键的概念:“进内存(Write Cache)”和“清空内存(Buffer Flushing)”。

你看到的属性面板里,其实有两个复选框:

启用设备上的写入缓存(默认开启):这个选项决定了数据是否先进入 Windows 内存。

关闭设备上的 Windows 写入高速缓存缓冲区刷新(你提到的这个):这个选项决定了 Windows 是否向服务器发送“立刻把缓存写进物理硬盘”的强制命令(Flush/FUA)。

下面为你还原勾选与不勾选时,到底发生了什么导致了“卡顿”:

状态一:不勾选(默认状态,会卡顿掉速)
当你不勾选这个选项时,Windows 会极其谨慎:

狂飙:数据先以 2.5G 的极速塞满 Windows 的内存缓冲区。

强制对账(发 Flush 指令):Windows 觉得内存里攒得够多了,于是通过网络给 PVE 发送一个 Flush 指令:“停下!现在立刻把你接收到的数据彻底写进底层的机械硬盘里,没写完之前不许接新数据!”

卡顿掉到 0:因为 PVE 底层的机械硬盘只有 100MB/s 的速度,要消化这几个 GB 的数据需要好几十秒。在这几十秒里,Windows 在干等 PVE 的“写完确认”信号,复制进度条随之彻底卡死,速度掉到 0。

状态二:勾选(关闭刷新,速度平滑但危险)
当你勾选这个选项时,你实际上剥夺了 Windows 发送强制对账指令(Flush)的权利:

狂飙并持续:数据依然先进入 Windows 内存,然后源源不断地通过 2.5G 网络发给 PVE。

不管底层死活:Windows 不再发送强制落盘的指令,它采取“发后不理”的态度。只要 PVE 的系统内存(Page Cache)还没满,它就继续发。

视觉不卡顿:因为没有了那个“必须等机械盘写完”的强制等待期,整个复制过程在 Windows 看来非常顺畅,也就不会出现断崖式掉到 0 的卡顿。

但是代价极其高昂:
因为 Windows 放弃了“对账”,如果在复制过程中,或者复制刚完成的几秒钟内,网线松了、PVE 意外重启、或者交换机闪断,那些存在 Windows 内存里以及 PVE 内存里、还没来得及真正写到机械硬盘上的数据,会瞬间灰飞烟灭,并大概率导致你的 iSCSI 分区文件系统直接损坏(变成 RAW 格式)。

iSCSI策略设计

服务端主外,客户端主内。

写入加速:交给 PVE 服务端 (dm-writecache) 在 PVE 端,配置一块 SSD 作为 dm-writecache。

作用:它作为唯一的数据接收大闸。Windows 传来的 50GB 大文件,全部由它安全、全速地吃下。就算此时 Windows 蓝屏、网线断了,数据只要进了 PVE 的 SSD 就绝对不会丢。

从 lvm-cache(即 dm-cache)切换到 dm-writecache,绝不能直接强行删除 SSD 逻辑卷。

因为 lvm-cache 默认可能是“回写(Writeback)”模式,SSD 里可能还存留着尚未刷入机械硬盘的“脏数据(Dirty Data)”。如果直接暴力破坏,你的整个 iSCSI 磁盘就会永久损坏。

正确的做法是让内核“平滑退役”旧缓存,然后再挂载新缓存。请按照以下安全步骤操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# 1.断开 Windows iSCSI 连接:防止新数据涌入.为了让旧缓存能尽快把数据排空,必须先切断数据源。去 Windows 端的 iSCSI 发起程序 中,将目标 断开连接 (Disconnect)。

# 2.安全解除并销毁旧的 lvm-cache:耗时取决于脏数据量,切勿强行中断.在 PVE 的 SSH 命令行中,输入以下命令解除缓存:
# 执行完毕后,旧的 SSD 缓存池会被自动销毁并释放空间,而你的原始数据卷会完好无损地保留下来。
lvconvert --uncache vg_home_data/lv_home_data
# Flushing 0 blocks for cache vg_home_data/lv_home_data.
# Logical volume "home_data_cache_cpool" successfully removed.
# Logical volume vg_home_data/lv_home_data is not cached.

# 暴力拆除旧逻辑卷 (LV):数据将在此步被销毁.强行删除损坏的缓存和主数据卷:
lvremove -f vg_home_data/home_writecache
lvremove -f vg_home_data/lv_home_data

# 创建主存储
yes | lvcreate -l 100%FREE -n lv_home_data vg_home_data /dev/sdc
# 2.在 SSD 上切出新的蓄水池:必须在命令末尾指定 /dev/nvme0n1.既然空间释放了,我们要在这个大 VG 里切出一块名为 nvme_writecache 的新逻辑卷。极其重要: 因为你的 VG 里同时有机械盘和固态盘,为了防止系统把这个蓄水池建在机械盘上,必须在命令最后指名道姓要求用 /dev/nvme0n1
lvcreate -n home_writecache -l 95%FREE vg_home_data /dev/nvme1n1

# 3.当你刚用 lvcreate 创建完 home_writecache 时,系统会自动把它“激活”(挂载在内核中备用)。
# 但是 dm-writecache 架构有一个硬性要求:它必须在底层彻底接管这个卷的格式。所以在此之前,我们需要手动把这个新建的缓存卷“停用”(从内核中剥离出休眠状态),才能把它当做外挂缓存装定到主数据卷上。
# 停用新建的缓存卷:让内核释放锁定.执行这条命令,把刚才新建的 SSD 卷变成 inactive(非激活)状态:
lvchange -a n vg_home_data/home_writecache

# 4.激活 dm-writecache 模式:将蓄水池盖在数据卷上.最后,将刚才创建的高速蓄水池,以 writecache 模式正式绑定到你的 2.73T 主数据卷上:
lvconvert --type writecache --cachevol vg_home_data/home_writecache vg_home_data/lv_home_data
# Erase all existing data on vg_home_data/home_writecache? [y/n]: y
# Using writecache block size 4096 for unknown file system block size, logical block size 512, physical block size 512.
# WARNING: unable to detect a file system block size on vg_home_data/lv_home_data
# WARNING: using a writecache block size larger than the file system block size may corrupt the file system.
# Use writecache block size 4096? [y/n]: y
# 第二个 y(Use writecache block size 4096?):
# 为什么会弹这个? 因为你的 lv_home_data 是通过 iSCSI 挂载给 Windows 使用的,底层格式化(如 NTFS)是在 Windows 端完成的。PVE 宿主机无法直接“透视”到里面的文件系统块大小,所以它报了个“无法检测”的警告。
# 为什么选 4096? Windows 的 NTFS 文件系统默认簇大小(Block Size)几乎全都是 4096 字节 (4KB)。dm-writecache 使用 4096 字节不仅性能最好,而且能与 Windows 的文件系统完美对齐。

# 1. 永久修改并发数为 4
lvchange --cachesettings 'writeback_jobs=4' vg_home_data/lv_home_data
# 2. 永久修改高低水位
lvchange --cachesettings 'high_watermark=30 low_watermark=10' vg_home_data/lv_home_data

# 3. 永久修改自动提交时间(可选,增加到 5秒/5000毫秒)
# dm-writecache 的元数据全部保存在“缓存盘(SSD)”的头部专属区域内,它压根不需要写进机械硬盘。这 5 秒发生了什么?
# 系统只是在 SSD 上极速记录一笔:“刚才收到的这几批数据块已经有效,索引已建好”。对于 SSD 而言,写几个 4KB 的元数据块耗时在 微秒(μs)级别,就像在便签纸上打了个勾,对 SSD 自身寿命和性能都几乎毫无察觉。
# 机械硬盘在这个瞬间需要做什么?什么都不需要做。 此时根本没有任何指令发给机械硬盘,机械硬盘完全不受这 5 秒定时器的干扰。
lvchange --cachesettings 'autocommit_time=5000' vg_home_data/lv_home_data

# 验证
dmsetup table vg_home_data-lv_home_data
# 如果没有生效必须热更新
dmsetup resume vg_home_data-lv_home_data

# 5.targetcli 找到旧的 LUN 和 fileio 名称
targetcli
ls

# 6.删除旧的 LUN 映射:必须先删 LUN,才能删底层存储.先解除 iSCSI 目标对旧 fileio 的占用。把下面命令中的 iqn... 替换为你自己的名称(多用 Tab 补全):
/iscsi/iqn.2026-07.local.pve:homedata/tpg1/luns/ delete lun0

# 7.删除旧的 fileio/block 设备:释放对 LVM 的 fileio/block 占用.删除旧的 fileio/block 后端存储(把 旧的名字 换成你刚才看到的名称):
/backstores/fileio delete fileio_home_data

# 8.创建全新的 Block 设备:指定我们刚才搞好的 LVM 路径.现在,创建一个纯粹的块级设备,命名为 block_home_data ,并指向你的主数据卷
# 在底层已经是 4K 的情况下,必须显式告诉 iSCSI 协议对外广播自己是 4K 盘。进入 targetcli,在创建块设备后,必须立即设置 block_size:
/backstores/block create name=block_home_data dev=/dev/vg_home_data/lv_home_data

/backstores/block/block_home_data set attribute block_size=4096

# 9.将新 Block 映射为 LUN:重新挂载给 iSCSI 目标.把刚刚建好的 block_home 挂载到你的 iSCSI 目标上
/iscsi create iqn.2026-07.local.pve:homedata
/iscsi/iqn.2026-07.local.pve:home-data/tpg1/acls create 你的Windows_IQN
/iscsi/iqn.2026-07.local.pve:homedata/tpg1/luns create /backstores/block/block_home_data

# 保存退出
saveconfig
exit

# 监控 lvm-cache dm-cache状态
watch -n 1 "lvs -o name,data_percent,cache_write_hits,cache_dirty_blocks vg_home_data/lv_home_data"

# 监控 lvm-cache dm-writecache 状态
# | 序号 | 示例值 | 字段名称 (Kernel 源码) | 详细功能与解读 |
# | :---: | :--- | :--- | :--- |
# | **1** | `0` | **Starting Sector** | 逻辑卷起始扇区号(通常为 0)。 |
# | **2** | `5860524032` | **Length (Sectors)** | 设备总扇区数(换算后约为 2.72 TB)。 |
# | **3** | `writecache` | **Target Type** | Device-mapper 驱动模块类型名称。 |
# | **4** | `0` | **Error / Status** | 错误码(`0` 为正常,非 0 表示处于错误/只读模式)。 |
# | **5** | `118312625` | **Total Blocks** | **缓存盘总块数**(按 4KB/块 计算,约 451.3 GB)。 |
# | **6** | `62277359` | **Free Blocks** | **当前空闲可用块数**(尚未被脏数据或读缓存占用的块)。 |
# | **7** | `0` | **Writeback in progress** | **当前正在进行回写锁定的块数**。 |
# | **8** | `842069694` | **Read Hits** | **读命中次数**(直接由 SSD 返回数据的累计 I/O 次数)。 |
# | **9** | `512795821` | **Read Misses** | **读未命中次数**(SSD 无缓存,穿透到底层机械盘读取的次数)。 |
# | **10** | `592773768` | **Write Hits** | **写命中次数**(目标块已在 SSD 中,直接覆盖更新的次数)。 |
# | **11** | `279529` | **Write Misses** | **写未命中次数**(写入全新位置,在 SSD 中新分配块的次数)。 |
# | **12** | `29003956` | **Dirty Blocks** | **【核心指标】当前积压的脏块数**(已入 SSD 但**未同步到底层机械盘**的数据,当前约 110.6 GB)。 |
# | **13** | `0` | **Clearing in progress** | 当前正在执行擦除/元数据清理的块数。 |
# | **14** | `563490289` | **Total Flushed Blocks** | **累计已刷盘块数**(历史成功写回到底层机械盘的总块数)。 |
# | **15** | `0` | **Discard Blocks** | 累计处理的 TRIM/Discard 丢弃块统计。 |
# | **16** | `1815036` | **Allocated Structures** | **当前分配的元数据结构体/内存节点数**(反映驱动占用的内存开销)。 |
# | **17** | `0` | **Corrupt / Bad Blocks** | 损坏/校验失败的坏块数(非 0 代表硬件故障或数据损坏)。 |
# 0 5860524032 writecache 0 118312625 62277359 0 842070036 512795837 592774496 279529 29004304 0 563490669 0 1815077 0
watch -n 1 'dmsetup status vg_home_data-lv_home_data'

watch -n 1 -t 'dmsetup status vg_home_data-lv_home_data | awk "{
total=\$5; free=\$6; in_flight=\$7;

used = total - free;

total_gb = total * 4 / 1024 / 1024;
free_gb = free * 4 / 1024 / 1024;
dirty_gb = used * 4 / 1024 / 1024;
inflight_mb = in_flight * 4 / 1024;

dirty_ratio = (total > 0) ? (used / total) * 100 : 0;

read_hit=\$8; read_miss=\$9; write_hit=\$10; write_miss=\$11;
total_reads = read_hit + read_miss;
read_hit_ratio = (total_reads > 0) ? (read_hit / total_reads) * 100 : 0;

printf \"%-22s: %.2f GB\n\", \"SSD 缓存总容量\", total_gb;
printf \"%-22s: %.2f GB (这就是脏数据池)\n\", \"当前占用 (未落盘)\", dirty_gb;
printf \"%-22s: %.1f%%\n\", \"当前脏数据水位\", dirty_ratio;
printf \"%-22s: %.2f MB\n\", \"正在写入机械盘的实时量\", inflight_mb;
printf \"%-22s: %.2f%% (命中: %d, 未命中: %d)\n\", \"读缓存命中率\", read_hit_ratio, read_hit, read_miss;
printf \"%-22s: %d 次 / %d 次\n\", \"写入命中/未命中\", write_hit, write_miss;
}"'

# 检查底层是否抛出内核错误
dmesg -T | grep -E "blocked|hung|writecache|I/O error|timeout"

# 查看硬盘状读写状态
iostat -x -z 1

#监控系统内存状态
watch -n 1 "free -h; echo '===================='; grep -E 'Dirty|Writeback' /proc/meminfo"

#如果达到水位之后,还是不落盘执行以下命令
dmsetup message vg_home_data-lv_home_data 0 flush

Q: 为什么挂在后会变成容量变大且不能访问?
A: 记得刚才我们配置 dm-writecache 时,按下的那两个 y 吗?当时系统提示:Use writecache block size 4096? [y/n]: y。你之前的旧架构(fileio)默认向 Windows 汇报的磁盘扇区大小是 512 字节。我们刚才建立的 dm-writecache 底层使用的是 4096 字节 (4K) 扇区。$4096 \div 512 = 8$。Windows 的分区表原本是用 512 字节一把尺子量出来的,现在突然尺子的刻度放大了 8 倍,Windows 傻乎乎地直接把你的磁盘容量也乘以了 8!(如果你原本的磁盘分区是 2T 左右,$2 \text{T} \times 8 = 16\text{T}$,完全对上了!)

读取加速:交给 Windows 客户端 (PrimoCache L2 读缓存)

在你日常使用的 Windows 电脑里,划出一块 SSD 给 PrimoCache 当作二级缓存(L2 Cache)。

关键设置:进入 PrimoCache,关闭“延迟写入”,将这块 L2 缓存配置为 100% 纯读取(Read-Data Only)。

作用:当你第一次从 iSCSI 看电影或读数据时,数据从 PVE 的机械硬盘流向 Windows,PrimoCache 会在后台悄悄把这些数据在 Windows 本地的 SSD 里存一份。明天你再去读这个文件,它直接从你电脑本地的 SSD 秒开,连 2.5G 的网络带宽都不占!

彻底静默运行,丢弃该命令输出的所有正常信息和报错信息,不向屏幕打印任何内容,也不发送通知

1
2
3
命令  >  /dev/null  2>&1
│ │ │
① ② ③

① >(重定向):

把前面的输出内容,转存/流向后面的目标(默认省略了数字 1,即 1>)。

② /dev/null(Linux 的黑洞 / 垃圾桶):

Linux 系统内置的一个特殊虚拟设备文件,写入它的任何数据都会被系统瞬间丢弃。

所以 > /dev/null 的意思就是:把所有的“标准正常输出(stdout)”全部扔进垃圾桶。

③ 2>&1(合并错误输出):

2 代表 stderr(错误输出);

> 代表 重定向;

&1 代表 引用 1 号通道(stdout)当前指向的地方。

整句的意思是:把“错误输出(2)”也丢到跟“正常输出(1)”一样的地方去(即扔进 /dev/null 黑洞)。

underscore框架设计

立即执行函数

使用立即执行函数,创建局部作用域,隔离环境并初始化代码

1
2
3
(function (global, factory) {

}(this, function () {}))

判断采用那种模块化规范导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// commomjs 规范
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
// amd 规范
typeof define === 'function' && define.amd ? define('underscore', factory) :
// 如果都不是则 直接挂在this上
(global = global || self, (function () {
var current = global._;
var exports = global._ = factory();
//防止多次引入冲突
exports.noConflict = function () {
global._ = current;
return exports;
};
}()));

创建根节点

1
2
3
4
5
// 建立根节点对象,self 在浏览器端, global 在服务端, this 在一些虚拟机中,使用self 代替 window 提供对 Webworker 的支持
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
Function('return this')() ||
{};

each方法

each 依赖的函数

一个内部函数,根据参数返回不同的回调函数的封装,一个复用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function optimizeCb(func, context, argCount) {

// 没有传入执行上下文, 直接返回函数
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1:
return function (value) {
return func.call(context, value);
};
// The 2-argument case is omitted because we’re not using it.
case 3:
return function (value, index, collection) {
return func.call(context, value, index, collection);
};
case 4:
return function (accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function () {
return func.apply(context, arguments);
};
}

isArrayLike

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;


// 简单获取属性值
function shallowProperty(key) {
return function (obj) {
return obj == null ? void 0 : obj[key];
};
}

var getLength = shallowProperty('length');

// 属性检查 数字格式,且不能超过数组最大值
function createSizePropertyCheck(getSizeProperty) {
return function (collection) {
var sizeProperty = getSizeProperty(collection);
return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
}
}

var isArrayLike = createSizePropertyCheck(getLength);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function each(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var _keys = keys(obj);
for (i = 0, length = _keys.length; i < length; i++) {
iteratee(obj[_keys[i]], _keys[i], obj);
}
}
return obj;
}

挂载方法 混合模式

依赖的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//内部方法,创建一个toString 基础测试器
function tagTester(name) {
var tag = '[object ' + name + ']';
return function (obj) {
return toString.call(obj) === tag;
};
}
var isFunction = tagTester('Function');
var isFunction$1 = isFunction;

//返回一个排序的 所有工具函数名的数组
function functions(obj) {
var names = [];
for (var key in obj) {
if (isFunction$1(obj[key])) names.push(key);
}
return names.sort();
}

定义underscore方法

  • 如果是underscore实例直接返回
  • 如果不是通过new操作符执行函数
  • 再次进入第二行的判断,这时已经是underscore的实例会继续往下执行
  • 在实例上挂在一个变量指向传入的对象
1
2
3
4
5
function _(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}

核心mixin 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function mixin(obj) {

// 循环所有导出方法的名称
each(functions(obj), function (name) {
// 每个名称对应的方法
var func = _[name] = obj[name];
// 在原型上挂载同样的方法
_.prototype[name] = function () {
//拿到上面挂载的 传入的对象
var args = [this._wrapped];
// 拼接为整个数组
Array.prototype.push.apply(args, arguments);
//直接用定义的内置方法执行
return chainResult(this, func.apply(_, args));
};
});
return _;
}

var allExports = {
each:each
}

var _$1 = mixin(allExports);
// Legacy Node.js API.
_$1._ = _$1;

return _$1;

所以可以用下面的方法执行方法

1
_([1,2,3]).each(function(item){console.log(item)});

测试环境karma

初始化

1
yarn init -y

实现第一个测试用例

index.js

1
2
3
var add = function(a){
return 1 + a
}

index.spec.js

1
2
3
4
5
describe('测试基本api',function(){
it('add',function(){
except(add(1)).toBe(2)
})
})

如何让上面的代码运行

安装karma测试框架

1
yarn add -D karma

配置package.json可以执行karma命令,如果不想配置可以安装 karma-cli

1
2
3
"scripts":{
"karma-init":"karma init"
}

执行命令初始化

1
yarn karma-init
  • Which testing framework do you want to use?

想要使用的单元测试框架 jasmine

  • Do you want to use Require.js

是否想要用Requirejs, 不需要

  • Do you want to capture any browsers automatically?

想要自动调用的浏览器 PhantomJS

PhantomJS是一个可编程的无头浏览器:一个完整的浏览器内核,包括js解析引擎,渲染引擎,请求处理等,但是不包括显示和用户交互页面的浏览器。

hantomJS的适用范围就是无头浏览器的适用范围。通常无头浏览器可以用于页面自动化,网页监控,网络爬虫等:

页面自动化测试:希望自动的登陆网站并做一些操作然后检查结果是否正常。
网页监控:希望定期打开页面,检查网站是否能正常加载,加载结果是否符合预期。加载速度如何等。
网络爬虫:获取页面中使用js来下载和渲染信息,或者是获取链接处使用js来跳转后的真实地址

  • What is the location of your source and test files

想要测试的文件位置,暂时留空

  • Should any of the files included by the previous patterns be excluded?

想要排除哪些文件暂时留空

  • Do you want Karma to watch all the files and run the tests on change?

是否需要监听文件改变, 暂时选no,不监听

下一步

打开生成的karma.conf.js文件

1
2
3
4
5
6
7
8
9
10
11
12
{
// 测试框架名称
frameworks: ['jasmine'],

//测试文件目录
files: [
"./test/unit/**/*.js",
"./test/unit/**/*.spec.js"
],

singleRun:true
}

在package.json中添加运行命令

1
2
3
4
 "scripts": {
"karma-init": "karma init",
"karma-start": "karma start"
}

安装无头浏览器和jasmine适配器

1
yarn add -D karma-jasmine karma-phantomjs-launcher phantomjs

编写测试用例

index.js

1
2
3
4
5
6
7
function add(a) {
if (a == 1) {
return a + 1;
} else {
return a + 2;
}
}

index.spec.js

1
2
3
4
5
describe('单元测试',function(){
it("contains spec with an expectation", function() {
expect(add(1)).toBe(2);
})
})

执行 karma-start 进行单元测试

覆盖率测试

安装 karma-coverage

1
yarn add -D karma-coverage

修改配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
preprocessors: {
"./test/unit/**/*.js":"coverage"
},

reporters: ['progress','coverage'],

coverageReporter: {
dir: 'doc/coverage',
reporters: [
// reporters not supporting the `file` property
{ type: 'html', subdir: 'report-html' },
]
}
}

执行再次执行 karma-start

UI测试

安装 backstop

1
yarn add -D backstop

添加 package.json 命令行

1
2
3
4
5
6
7
{
"scripts": {
"backstop-init": "backstop init",
"backstop-start": "backstop test"
}
}

执行 backstop-init 生成文件目录

修改文件目录配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
"id": "baidu",

// 设计图分辨率设置
"viewports": [
{
"label": "phone",
"width": 375,
"height": 667
},
{
"label": "tablet",
"width": 1024,
"height": 768
}
],

//调用 puppeteer 库的脚本
"onBeforeScript": "puppet/onBefore.js",
"onReadyScript": "puppet/onReady.js",
"scenarios": [
{
"label": "baidu home",
"cookiePath": "backstop_data/engine_scripts/cookies.json",
"url": "https://garris.github.io/BackstopJS/",
"referenceUrl": "",
"readyEvent": "",
"readySelector": "",
"delay": 0,
"hideSelectors": [],
"removeSelectors": [],
"hoverSelector": "",
"clickSelector": "",
"postInteractionWait": 0,
"selectors": [],
"selectorExpansion": true,
"expect": 0,
"misMatchThreshold" : 0.1,
"requireSameDimensions": true
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"engine_scripts": "backstop_data/engine_scripts",
"html_report": "doc/backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
},
"report": ["browser"],
"engine": "puppeteer",
"engineOptions": {
"args": ["--no-sandbox"]
},
"asyncCaptureLimit": 5,
"asyncCompareLimit": 50,
"debug": false,
"debugWindow": false
}

单元测试理论

TDD 测试驱动开发

  • 首先,开发者在码业务前写一些测试用例
  • 运行这些测试用例。结果肯定是运行失败,因为测试用例中的业务逻辑还没实现嘛
  • 开发者实现测试用例中的业务逻辑
  • 再运行测试用例, 如果开发者代码能力不错,这些测试用例应该可以跑通了(pass)
  • 对业务代码及时重构,包括增加注释,清理重复等。因为没人比开发者自己更了解哪些代码会对哪些部分造成影响从而导致测试失败(fail)

我们通过举例来了解一下如何实践TDD。例子中的代码可以从github上获取tdd-vs-bdd。将代码clone下来,执行命令npm install && grunt

假设我们想写一个计算阶乘的函数(这是一个很刻意的例子,但是这个例子对我们指出TDD和BDD的区别很有帮助)。TDD的常用方式是运行某函数,然后断言结果满足某个值。

在阶乘的例子中,我们使用的javascript测试框架是Mocha。废话不说,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var assert = require('assert'),
factorial = require('../index');

suite('Test', function (){
setup(function (){
// Create any objects that we might need
});

suite('#factorial()', function (){
test('equals 1 for sets of zero length', function (){
assert.equal(1, factorial(0));
});

test('equals 1 for sets of length one', function (){
assert.equal(1, factorial(1));
});

test('equals 2 for sets of length two', function (){
assert.equal(2, factorial(2));
});

test('equals 6 for sets of length three', function (){
assert.equal(6, factorial(3));
});
});
});

显然上述测试会失败,因为我们尚未实现函数功能。所以接下来我们需要实现满足上述测试用例的阶乘函数。代码如下

1
2
3
4
5
6
module.exports = function (n) {
if (n < 0) return NaN;
if (n === 0) return 1;

return n * factorial(n - 1);

现在我们再次运行测试用例,所有的case都跑通了! 这就是TDD的使用方式。

BDD 行为驱动开发

BDD旨在消除TDD过程中可能造成的问题。

与TDD相比,BDD是通过编写行为和规范来驱动软件开发。 行为和规范可能看起来与测试非常相似,但是它们之间却有着微妙但重要的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var assert = require('assert'),
factorial = require('../index');

describe('Test', function (){
before(function(){
// Stuff to do before the tests, like imports, what not
});

describe('#factorial()', function (){
it('should return 1 when given 0', function (){
factorial(0).should.equal(1);
});

it('should return 1 when given 1', function (){
factorial(1).should.equal(1);
});

it('should return 2 when given 2', function (){
factorial(2).should.equal(2);
});

it('should return 6 when given 3', function (){
factorial(3).should.equal(6);
});
});

after(function () {
// Anything after the tests have finished
});
});

敏捷开发

敏捷开发以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发。在敏捷开发中,软件项目在构建初期被切分成多个子项目,各个子项目的成果都经过测试,具备可视、可集成和可运行使用的特征。换言之,就是把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态。

偏函数和函数柯里化

偏函数 (Partial application)

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。(什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。)

没有上下文的偏函数
1
2
3
4
5
6
7
const partial = (fn, ...args) => {
return (...args2) => fn.call(this, ...args, ...args2)
}

console.log(partial(function (a, b, c, d) {
return a + b + c + d
}, 1, 2)(3, 4))
bind 实现
  • 类型判断,错误处理
  • 缓存一级参数
  • 定义返回的新函数
  • 处理原型链
  • 绑定新函数的执行上下文,判断是否通过new调用
1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = function (ctx) {
if (typeof this !== 'function') throw new Error();
var args = Array.prototype.slice.call(arguments, 1);
var toBind = this;
var fn = function _fn() {
args = args.concat(Array.prototype.slice.call(arguments, 0))
toBind.apply(this instanceof _fn ? this : ctx, args)
}
if (toBind.prototype) {
fn.prototype = Object.create(toBind.prototype)
}
return fn;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.bind = function (ctx) {
if (typeof this !== 'function') throw new Error();
var args = Array.prototype.slice.call(arguments, 1);
var toBind = this;
var noop = function(){}
var fn = function _fn() {
args = args.concat(Array.prototype.slice.call(arguments, 0))
toBind.apply(noop.prototype.isPrototypeOf(this) ? this : ctx, args)
}
if (toBind.prototype) {
noop.prototype = toBind.prototype;
}
fn.prototype = new noop()
return fn;
}

柯里化 (Currying)

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

ES6实现
1
2
const curry = (fn, args = []) =>
fn.length === args.length ? fn(...args) : (...args2) => curry(fn, [...args, ...args2])
反柯理化

使用箭头函数不能绑定函数的this

1
const uncurry = (fn) => (...args) => fn.apply(this, args)
1
2
3
4
5
6
Function.prototype.uncurring = function () {
var self = this;
return function () {
return self.apply(this, arguments);
};
};

偏函数与柯里化区别

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

.bashr 和 .profile的区别

要搞清bashrc与profile的区别,首先要弄明白什么是交互式shell和非交互式shell,什么是login shell 和non-login shell。

交互式模式

shell等待你的输入,并且执行你提交的命令。这种模式被称作交互式是因为shell与用户进行交互。

这种模式也是大多数用户非常熟悉的:登录、执行一些命令、签退。当你签退后,shell也终止了

非交互式模式

shell不与你进行交互,而是读取存放在文件中的命令,并且执行它们。当它读到文件的结尾,shell也就终止了。

bashrc与profile都用于保存用户的环境信息,bashrc用于非交互式non-loginshell,而profile用于交互式login shell。

系统中存在许多bashrc和profile文件:

  • /etc/profile 此文件为系统的每个用户设置环境信息,当第一个用户登录时,该文件被执行.并从/etc/profile.d目录的配置文件中搜集shell的设置.

    每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次!默认情况下,它设置一些环境变量,然后执行用户的.bashrc文件.

  • /etc/bashrc:为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取。有些linux版本中的/etc目录下已经没有了bashrc文件。

    该文件包含专用于某个用户的bash shell的bash信息,当该用户登录时以及每次打开新的shell时,该文件被读取.另外,/etc/profile中设定的变量(全局)的可以作用于任何用户,而~/.bashrc等中设定的变量(局部)只能继承/etc/profile中的变量,他们是”父子”关系.

/etc/profile,/etc/bashrc 是系统全局环境变量设定

/.profile,/.bashrc用户家目录下的私有环境变量设定

当登入系统时候获得一个shell进程时,其读取环境设定档有三步

  1. 首先读入的是全局环境变量设定档/etc/profile,然后根据其内容读取额外的设定的文档,如/etc/profile.d和/etc/inputrc

  2. 然后根据不同使用者帐号,去其家目录读取/.bash_profile,如果这读取不了就读取/.bash_login,这个也读取不了才会读取~/.profile,这三个文档设定基本上是一样的,读取有优先关系

  3. 然后在根据用户帐号读取/.bashrc至于/.profile与~/.bashrc的不区别都具有个性化定制功能

~/.profile可以设定本用户专有的路径,环境变量,等,它只能登入的时候执行一次
~/.bashrc也是某用户专有设定文档,可以设定路径,命令别名,每次shell script的执行都会使用它一次

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信