文件自动同步服务器

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env node

const shell = require('shelljs')
const chokidar = require('chokidar')
const home = require('user-home')
const path = require('path')

const WORKSPACE_PATH = path.resolve(home, 'Workspace');

const expectPath = path.join(__dirname, './expect.exp')
const to = 'root@144.xxx.xxx.92:/home/backup'

chokidar
.watch('./index.js', {
ignored: (path) => path.includes('node_modules') || path.includes('.git')
})
.on('all', (event, path) => {
console.log(event, path);
shell.exec(`expect ${expectPath} ${path} ${to}`)
});

expect.exp

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
#expect 不能写错
#!/usr/bin/expect

set from [lindex $argv 0]
set to [lindex $argv 1]

set timeout 30

spawn bash -c "scp $from $to"

# 特殊符号需要转移 回车符号不需要
# \ 需转义为 \\\
# } 需转义为 \}
# [ 需转义为 \[
# $ 需转义为 \\\$
# ` 需转义为 \`
# " 需转义为 \\\"
# ( 需转义为 \(


expect {
"*password:" { send -- "j\(S7?xx#ky6\[0n=R\r" }
}

# 必须要加这一句,在执行完毕后把持交互状态,把控制台,这时候就可以进行你想要进行的操作了。如果没有这一句,在登陆完成之后就会退出,而不是留在远程终端上。
interact

性能优化

性能优化开篇

Html 数量控制 -> 压缩合并(30k为标准,请求变少) -> 开启CDN, gzip,brotli压缩方式 -> 服务端开启keep-alive http2 -> 测速 -> 缓存

离线缓存

localStorage 5M 低端机型,不能占用过高

script 放在底部,不会影响dom的解析,但是会影响dom的渲染

1
2
3
4
5
6
<body>
<h1>标题</h1>
<script>
alert(0)
</script>
</body>

同样css不会影响dom的解析,但是影响dom的渲染

1
2
3
4
5
6
7
8
9
10
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./css">
</head>

<body>
<h1>标题</h1>
</body>

css 加载会阻塞js的执行,因为不知道js中是否使用了css, 但是不会影响dom的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./css">
</head>

<body>
<h1>标题</h1>
<script>
alert(0)
</script>
</body>

css的加载不会影响dom ready,但是如果css,下面有js脚本,则不会执行dom ready,因为不知道是否使用了css样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
document.addEventlister('DomContentloaded',()=>{console.log('ready')})
</script>
<link rel="stylesheet" href="./css">
</head>

<body>
<h1>标题</h1>
<!-- <script>
alert(0)
</script> -->
</body>

渲染中的性能优化

CLI

命令行工具

基于文本查看,处理,操作计算机上面文件的程序

开发环境

webpack 打包 编译 图片压缩

命令

vue [options]

主命令 + 子命令 + 参数

init generate a new project from a template
list list available official templates
build prototype a new project
create (for v3 warning only)
help [cmd] display help for [cmd]

通过用户的配置拉取远程的模板,来生成模板

vue list 查看模板的种类

Available official templates:
★ browserify - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing.
★ browserify-simple - A simple Browserify + vueify setup for quick prototyping.
★ pwa - PWA template for vue-cli based on the webpack template
★ simple - The simplest possible Vue setup in a single HTML file
★ webpack - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction.
★ webpack-simple - A simple Webpack + vue-loader setup for quick prototyping.

borwserify 是有另一种打包规范,用于处理使用CMD打包规范,引用的模块

初始化

vue help init 查看初始化命令

Usage: vue-init [project-name]

Options:
-c, –clone use git clone
–offline use cached template
-h, –help output usage information
Examples:

# create a new project with an official template
$ vue init webpack my-project

# create a new project straight from a github template
$ vue init username/repo my-project

流程:

  • vue 输入命令 命令内部初始化

  • 获取用户配置:交互

  • 解析配置 基础配置+用户配置=配置

  • 生成项目文件 通过配置来拉取远程官方模板 + 自己写的模板
    自己写的模板必须包含 :meta.js / json
    prompts=>收集弹框信息
    helpers=>模板引擎的扩展
    complete=>钩子函数
    metalsmith=>站点生成器 通过配置和模板生成文件

    还必须包含模板文件:template

  • 完善工作

  • commander 完整的 node.js 命令行解决方案

  • Inquirer 弹窗交互

  • chalk 命令行美化工具

  • ora 命令行加载动画

  • execa 持续集成 提交到主分支

  • lerna 处理多包相互依赖

目录
  • docs 文档

  • scripts 脚本

  • packages => lerna

  • lib 核心逻辑

  • package.json
    bin 放置用户自定义命令

    为什么没有全局安装的命令可以使用 npm run 来执行,但是不能直接调用?

    如果一个命令想要全局执行,需要添加到全局的环境变量 PATH 中

    在执 npm run 或者 yarn 的时候,会自动在node_modules中查找需要执行的文件,通过npm link 软连接,添加到全局的环境变量PATH中,在执行完成后再删除

    全局安装的命令,会安装在 /usr/local/node12.18.4/lib/node_modules/,执行的时候会自动link,和scripts中写的命令同理

    bin 文件夹下面的命令需要手动link,在入口文件改变时需要重新link

    所以写一个命令的步骤: 1)创建bin文件夹,添加文件 2) 文件头部添加 #!/usr/bin/env node 表示可执行文件 3) link 到全局

vue-cli

vue-list 请求模板

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
#!/usr/bin/env node

const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')

// 监听ctrl + c退出事件
process.on('exit', () => {
console.log()
})

//请求模板使用到的包
// 如果没有报错,拼接包的名字 返回上面的列表
request({
url: 'https://api.github.com/users/vuejs-templates/repos',
headers: {
'User-Agent': 'vue-cli'
}
}, (err, res, body) => {
if (err) logger.fatal(err)
const requestBody = JSON.parse(body)
if (Array.isArray(requestBody)) {
console.log(' Available official templates:')
console.log()
requestBody.forEach(repo => {
console.log(
' ' + chalk.yellow('★') +
' ' + chalk.blue(repo.name) +
' - ' + repo.description)
})
} else {
console.error(requestBody.message)
}
})

vue-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
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env node

const download = require('download-git-repo')
const program = require('commander')
const exists = require('fs').existsSync
const path = require('path')
const ora = require('ora')
const home = require('user-home')
const tildify = require('tildify')
const chalk = require('chalk')
const inquirer = require('inquirer')
const rm = require('rimraf').sync
const logger = require('../lib/logger')
const generate = require('../lib/generate')
const checkVersion = require('../lib/check-version')
const warnings = require('../lib/warnings')
const localPath = require('../lib/local-path')

const isLocalPath = localPath.isLocalPath
const getTemplatePath = localPath.getTemplatePath

program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')

/**
* Help.
*/

program.on('--help', () => {
console.log(' Examples:')
console.log()
console.log(chalk.gray(' # create a new project with an official template'))
console.log(' $ vue init webpack my-project')
console.log()
console.log(chalk.gray(' # create a new project straight from a github template'))
console.log(' $ vue init username/repo my-project')
console.log()
})

/**
* Help.
*/

function help () {
program.parse(process.argv)
if (program.args.length < 1) return program.help()
}
help()

/**
* Settings.
*/

let template = program.args[0]
const hasSlash = template.indexOf('/') > -1
const rawName = program.args[1]
const inPlace = !rawName || rawName === '.'
const name = inPlace ? path.relative('../', process.cwd()) : rawName
const to = path.resolve(rawName || '.')
const clone = program.clone || false

const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
template = tmp
}

/**
* Padding.
*/

console.log()
process.on('exit', () => {
console.log()
})

if (inPlace || exists(to)) {
inquirer.prompt([{
type: 'confirm',
message: inPlace
? 'Generate project in current directory?'
: 'Target directory exists. Continue?',
name: 'ok'
}]).then(answers => {
if (answers.ok) {
run()
}
}).catch(logger.fatal)
} else {
run()
}

/**
* Check, download and generate the project.
*/

function run () {
// check if template is local
if (isLocalPath(template)) {
const templatePath = getTemplatePath(template)
if (exists(templatePath)) {
generate(name, templatePath, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
} else {
logger.fatal('Local template "%s" not found.', template)
}
} else {
checkVersion(() => {
if (!hasSlash) {
// use official templates
const officialTemplate = 'vuejs-templates/' + template
if (template.indexOf('#') !== -1) {
downloadAndGenerate(officialTemplate)
} else {
if (template.indexOf('-2.0') !== -1) {
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
return
}

// warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
downloadAndGenerate(officialTemplate)
}
} else {
downloadAndGenerate(template)
}
})
}
}

/**
* Download a generate from a template repo.
*
* @param {String} template
*/

function downloadAndGenerate (template) {
const spinner = ora('downloading template')
spinner.start()
// Remove if local template exists
if (exists(tmp)) rm(tmp)
download(template, tmp, { clone }, err => {
spinner.stop()
if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
generate(name, tmp, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
})
}

O(03).数组中重复的数字

LeetCode

注意

暴力解法

使用 map 或 set

1
2
3
4
5
6
7
var findRepeatNumber = function (nums) {
var map = new map();
for (let n of nums) {
if (map[n]) return n;
map[n] = true
}
};

原地替换

  • 题目描述 (长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内 ), 说明每个元素都应该与自己的下标相等,都有自己的位置

  • 通过循环把每个元素放回自己的位置,如果发现被相同的元素占用,表示重复

1
2
3
4
5
6
7
8
9
10
11
var findRepeatNumber = function (nums) {
var index = 0;
while (nums[index] !== undefined) {
var temp = nums[index];
if (temp === index) { index += 1; continue };
if (nums[temp] === temp) return temp;
nums[index] = nums[temp];
nums[temp] = temp;
}
return -1;
};

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

BFF 实践

BFF

BFF,即 Backend For Frontend(服务于前端的后端),也就是服务器设计 API 时会考虑前端的使用,并在服务端直接进行业务逻辑的处理,又称为用户体验适配器。BFF 只是一种逻辑分层,而非一种技术,虽然 BFF 是一个新名词,但它的理念由来已久。

通常一个页面的请求包含了多个不同的请求,用于页面组件的渲染

同时为了保障 Android,iOS,以及 Web 端的不同需求,需要为不同的平台写不同的 API 接口,而每当值发生一些变化时,需要 Android,iOS,Web 做出修改。

有了 BFF 这一层时,我们就不需要考虑系统后端的迁移。后端发生的变化都可以在 BFF 层做一些响应的修改

BFF 场景

多端应用

我们在设计 API 时会考虑到不同设备的需求,也就是为不同的设备提供不同的 API,虽然它们可能是实现相同的功能,但因为不同设备的特殊性,它们对服务端的 API 访问也各有其特点,需要区别处理。

服务聚合

随着微服务的兴起,原本在同一个进程内运行的业务流程被拆分到了不同的服务中。这在增加业务灵活性的同时,也让前端的调用变得更复杂。BFF 的出现为前端应用提供了一个对业务服务调用的聚合点,它屏蔽了复杂的服务调用链,让前端可以聚焦在所需要的数据上,而不用关注底层提供这些数据的服务。

实战中的玩法

访问控制

例如,服务中的权限控制,将所有服务中的权限控制集中在 BFF 层,使下层服务更加纯粹和独立。

应用缓存

项目中时常存在一些需要缓存的临时数据,此时 BFF 作为业务的汇聚点,距离用户请求最近,遂将该缓存操作放在 BFF 层。

第三方入口

在业务中需要与第三交互时,将该交互放在 BFF 层,这样可以只暴露必要信息给第三方,从而便于控制第三方的访问。

初始化项目

项目目录结构划分

package.json 生命周期 并行执行

安装webpack

1
yarn add -D webpack-cli webpack

package.json 文件添加

1
2
3
4
"scripts": {
"test": "echo test",
"pretest": "echo pretest"
},

执行 yarn test

1
2
3
4
5
6
yarn run v1.22.10
$ echo pretest
pretest
$ echo test
test
Done in 0.05s.

并行执行,不能保证顺序

1
2
3
4
5
"scripts": {
"test1": "echo test1",
"test2": "echo test2",
"test": "yarn test1 & yarn test2"
}
1
2
3
4
5
6
7
8
yarn test
yarn run v1.22.10
$ yarn test1 & yarn test2
$ echo test2
$ echo test1
test2
test1
Done in 0.25s.
scripy

使用scripty拆分复杂命令

1
yarn add -D scripty

package.json

1
2
3
4
5
"scripts": {
"test:one": "scripty",
"test:two": "scripty",
"test": "scripty"
}

按照命令建立文件夹

执行 yarn test test文件夹下面的所有命令

package.json 定义公共参数

package.json

1
2
3
"config":{
"port":9999
}

通过变量在命令配置中使用

script/test/one.sh

1
echo $npm_package_config_port

执行 yarn test 打印端口

1
2
3
4
5
6
7
8
9
yarn run v1.22.10
$ scripty
scripty > Executing "/home/supreme/Workspace/mvc/scripts/test/one.sh":

9999
scripty > Executing "/home/supreme/Workspace/mvc/scripts/test/two.sh":

2
Done in 0.13s.
jscpd 代码重复率检查
1
yarn add -D jscpd

添加配置文件.jscpd.json

1
2
3
4
{
"threshold": 0,
"reporters": ["html", "console"]
}

scripts

1
jscpd --min-lines 1 --output ./doc/jscpd --pattern "src/**/*.js"
webpack配置

建立配置文件夹 config,通过内置方法拿到配置参数

1
process.env.NODE_ENV

安装 yargs,以对象的形式更方便的获取命令参数

安装 webpack-merge 合并公共配置和定制配置

art-template

使用art-template作为后端模板,用于服务端渲染html
配置和swig 模板类似

1
yarn add art-template koa-art-template
打包思路

通过webpack打包前端代码

html-webpack-plugin 处理前端模板,放到指定位置

因为模板中插入js代码所以需要通过编写插件处理

后端模板通过 gulp 处理

处理模块化规范,删除无用的代码

webpack.js

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
const glob = require('glob')
const { argv } = require('yargs')
const path = require('path')
const files = glob.sync("./src/web/views/**/*.js")
const htmls = glob.sync("./src/web/views/**/*.art")
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const CustomInjectPlugin = require('./src/service/config/CustomInjectPlugin');
const mode = argv.mode;
if (!files.length) return;

const entry = files.reduce((entry, path) => {
const match = path.match(/(\w+)\/(\w+)\.js$/);
entry[match[1]] = match.input;
return entry;
}, {});

const htmlPlugin = htmls.map(path => {
const match = path.match(/(\w+)\/(\w+)\.art$/);
return new HtmlWebpackPlugin({
filename: `${match[1]}.art`,
template: match.input,
hash: true,
chunks: ['runtime', match[1]],
inject: false
})
})
module.exports = {
entry,
mode,
output: {
filename: '[name]_[contentHash].js',
path: __dirname + '/dist'
}
plugins: [
new CleanWebpackPlugin(),
...htmlPlugin,
new CustomInjectPlugin()
],
optimization: {
runtimeChunk: {
name: 'runtime'
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
}
}
]
},
resolve: {
alias: {
"@": path.resolve(__dirname, 'src/web')
}
}
}

CustomInjectPlugin.js

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
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pluginName = 'CustomInjectPlugin';

class CustomInjectPlugin {
js = ''
apply(compiler) {
compiler.hooks.compilation.tap('pluginName', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
pluginName,
(data, cb) => {
const { assets: { js } } = data;
this.js = js.map(src => src.replace('@', './')).join('');
cb(null, data)
}
)
// Static Plugin interface |compilation |HOOK NAME | register listener
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
pluginName, // <-- Set a meaningful name here for stacktraces
(data, cb) => {
const { html } = data;
data.html = html.replace(/!script!/, this.js);
this.js = ''
cb(null, data)
}
)
})
}
}

module.exports = CustomInjectPlugin;

gulpfile.js

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
const { series, src, dest } = require('gulp');
var plugins = require('gulp-load-plugins')();
const replace = require('@rollup/plugin-replace');

const jspath = './src/service/**/*.js';

// 开发环境 监听文件变化, 处理模块化规范
function es6(cb) {
plugins.watch(jspath, { ignoreInitial: false },
function () {
return src(jspath)
.pipe(plugins.babel({
plugins: [
"@babel/plugin-transform-modules-commonjs",
"@babel/plugin-transform-runtime"
]
}))
.pipe(dest('dist'))
}
)
.pipe(dest('build'));
return cb()
}

function es6dev() {
return src(jspath)
.pipe(plugins.babel({
ignore: ['./src/service/config/index.js'],
plugins: [
//处理模块化规范
"@babel/plugin-transform-modules-commonjs",
"@babel/plugin-transform-runtime"
]
}))
.pipe(dest('dist'))
}

function codeClean(cb) {
return src(jspath)
// transform the files here.
.pipe(plugins.rollup({
// any option supported by Rollup can be set here.
input: './src/service/config/index.js',
output: {
format: 'cjs'
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
}))
.pipe(dest('dist'))
}

exports.dev = series(es6);
exports.default = series(es6dev, codeClean);

sonarQube

最后更新

2024-12-11

安装数据库

安装 postgresql[文档]

1
apt install postgresql

创建 schema

1
2
3
4
5
6
7
# 登录数据库
sudo -u postgres psql

# 创建数据库,必须使用 UTF-8 编码
CREATE DATABASE sonarqube_db ENCODING 'UTF8';
# 链接到新的数据库
\c sonarqube_db # 进入数据库

创建用户 sonarqube

1
CREATE USER sonarqube WITH PASSWORD 'YourSecurePassword';

创建 scheme,并赋予 sonarqube 用户所有权限

1
CREATE SCHEMA sonarqube_schema AUTHORIZATION sonarqube;

由于没有使用默认的 schema,必须要设置 search_path

1
ALTER USER sonarqube SET search_path to sonarqube_schema;

配置 java 环境

安装 java 环境,[文档]

安装依赖包

1
apt install -y wget apt-transport-https gpg

下载 Eclipse Adoptium GPG 密钥

1
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null

配置库信息

1
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list

安装

1
2
apt update
apt-get install temurin-21-jdk

安装前环境准备

确认信息

一个进程可能拥有的最大内存映射区域数(vm.max_map_count)大于等于 524288。
打开的文件描述符的最大数目(fs.file-max)大于或等于 131072。
运行 SonarQube Server 的用户至少可以打开 131072 个文件描述符
运行 SonarQube Server 的用户至少可以打开 8192 个线程

使用以下命令查看信息

1
2
3
4
5
6
7
sysctl vm.max_map_count

sysctl fs.file-max

ulimit -n

ulimit -u

修改配置

1
2
3
4
5
6
# 创建一个新的配置文件
/etc/sysctl.d/99-sonarqube.conf

# 添加
vm.max_map_count=524288
fs.file-max=131072
1
2
3
4
5
6
7
# 创建一个新的配置文件
/etc/security/limits.d/99-sonarqube.conf

# 添加
sonarqube   -   nofile   262144

sonarqube   -   nproc    16384

在 Linux 内核上启用 seccomp

1
2
3
4
5
6
grep SECCOMP /boot/config-$(uname -r)

# 如果您的内核有seccomp,将看到以下内容
# CONFIG_HAVE_ARCH_SECCOMP_FILTER=y
# CONFIG_SECCOMP_FILTER=y
# CONFIG_SECCOMP=y

安装 sonar

下载,并解压,路径中不能有 .开头的文件夹

1
unzip sonarqube-25.3.0.104237.zip

编辑数据库链接信息

1
2
3
4
5
vi <sonarqubeHome>/conf/sonar.properties

sonar.jdbc.username=sonarqube
sonar.jdbc.password=mypassword
sonar.jdbc.url=jdbc:postgresql://localhost:5432/sonarqube_db?currentSchema=sonarqube_schema

配置 Elasticsearch 存储路径

1
2
3
4
vi <sonarqubeHome>/conf/sonar.properties

sonar.path.data=/var/sonarqube/data
sonar.path.temp=/var/sonarqube/temp

启动服务

1
2
3
4
<sonarqubeHome>/bin/linux-x86-64/sonar.sh start

# http://localhost:9000
# admin/admin

FAQ

检查日志
1
cat <sonarqubeHome>/logs/sonar.log
Startup error: ‘can not run elasticsearch as root’

修改 sonar 安装目录权限

1
2
sudo chown -R sonar:sonar /opt/sonarqube-25.3.0.104237/
sudo chown -R sonar:sonar /var/sonarqube
Process exited with exit value [ElasticSearch]: 143

143 错误 99% 与前端启动相关,检查 web.log

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

查找占用端口

可以使用 ssnetstat 命令 后面加 -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
# 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 的文件系统完美对齐。

# 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 状态
watch -n 1 'dmsetup status vg_home_data-lv_home_data'

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

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 的网络带宽都不占!

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信