Chemmy's Blog

chengming0916@outlook.com

环境准备

本地安装 Git NodeJS

检查环境

1
2
3
4
5
git -v

node -v

npm -v

切换镜像站,具体参考NPM配置国内源

1
npm config set registry https://registry.npmmirror.com

Hexo环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pnpm install -g hexo-cli                # 安装Hexo cli工具

hexo init # 初始化博客环境
npm install # 安装依赖库

# 插件
npm install hexo-asset-img # 头像
npm install hexo-auto-category # 自动分类
npm install hexo-generator-searchdb # 生成搜索数据库
npm install hexo-backlink # Obsdian链接转换
npm install hexo-deploy-git # git自动发布
npm install hexo-theme-next # hexo NexT主题
npm install hexo-server # hexo服务器
npm install hexo-next-giscus # giscus评论组件
npm install hexo-wordcount # 字数统计

Hexo 配置

参考官方文档

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
...
theme: next # 配置主题next

giscus: # 评论配置
enable: true
repo: # Github repository name
repo_id: # Github repository id
category: # Github discussion category
category_id: # Github discussion category id
# Available values: pathname | url | title | og:title
mapping: title
# Available values: 0 | 1
reactions_enabled: 1
# Available values: 0 | 1
emit_metadata: 1
# Available values: light | light_high_contrast | light_protanopia | light_tritanopia | dark | dark_high_contrast | dark_protanopia | dark_tritanopia | dark_dimmed | preferred_color_scheme | transparent_dark | noborder_light | noborder_dark | noborder_gray | cobalt | purple_dark
theme: light
# Available values: en | zh-CN
lang: zh-CN
# Place the comment box above the comments
input_position: bottom
# Load the comments lazily
loading: lazy

deploy: # 发布配置
- type: git
repo: # 仓库发布地址
branch: main # 发布分支
name: # git用户名 git config user.name <username>
email: # git邮箱 git config user.email <email>
...

注意: 评论部分需要借助Github Discussions, 参考Hexo博客配置Giscus评论

Hexo主题配置

安装主题后从npm_modules/<主题名>/文件夹中复制_config.yml到博客根目录并重命名为_config.next.yml,当博客deploy时回自动应用主题配置,一下主题修改都基于此文件进行。

设置语言

NexT主题支持多种语言,只需要编辑_config.next.yml中的language设置即可

语言 代码 设定示例
English en language: en
简体中文 zh-CN(注:zh-Hans已经无法使用) language: zh-CN
Frangais fr-FR language: fr-FR
Portugues pt language: pt
或者
language:pt-BR
繁體中文 zh-hk
或者
zh-tw
language: zh-hk
Pycckmi 93bIK ru language: ru
Deutsch de language: de
日本語 ja language: ja
Indonesian id language: id
Korean ko language: ko
如果需要添加非内置的字段需要手动添加翻译文件,例如中文的翻译文件路径为node_modules/next/languages/zh-CN.yml

设置关于

source/about/index.md中添加如下内容

1
2
3
4
5
6
---
title: 关于
date: 2025-08-27 00:00:00
---

<个人信息>

选择Scheme

Scheme 是 NexT 提供的一种特性,借助于 Scheme,NexT 为你提供多种不同的外观。同时,几乎所有的配置都可以 在 Scheme 之间共用。目前 NexT 支持三种 Schem

  • Muse - 默认 Scheme
  • Mist - Muse 的紧凑版本
  • Pisces - 双栏 Scheme
  • Gemini

菜单配置

菜单配置包括三个部分,第一是菜单项(名称和链接),第二是菜单项的显示文本,第三是菜单项对应的图标。 NexT 使用的是 Font Awesome 提供的图标, Font Awesome 提供了 600+ 的图标,可以满足绝大的多数的场景,同时无须担心在 Retina 屏幕下 图标模糊的问题。

1
2
3
4
5
6
7
8
menu: home: / || home 
categories: /categories/ || th
archives: /archives/ || archive
tags: /tags/ || tags
#schedule: /schedule/ || calendar
#sitemap: /sitemap.xml || sitemap
#commonweal: /404/ || heartbeat
about: /about/ || user

NexT 默认的菜单项有(标注 * 的项表示需要手动创建这个页面):

注意: 若站点运行在子目录中,请将链接前缀的 / 去掉。

键值 设定值 显示文本(简体中文)
home home: / 主页
archives archives: /archives 归档页
categories categories: /categories 分类页 *
tags tags: /tags 标签页 *
about about: /about 关于页面*
commonweal commonweal: /404.html 公益 404 !

侧栏配置

默认情况下,侧栏仅在文章页面(拥有目录列表)时才显示,并放置于右侧位置。配置具体如下

1
2
3
4
5
6
7
...

sidbar:
position: left # 配置侧栏居左
display: post # 侧栏显示行为

...

侧栏显示位置支持

  • left: 居左显示
  • right: 居右显示

侧栏显示行为支持

  • post 默认行为,在文章页面(拥有目录列表)时显示
  • always 所有页面都显示
  • hide 在所有页面中都隐藏(可以手动展开)
  • remove 完全移除

注册Github账号,Gitea账号(可选)
[^注] Github由于网络问题会经常无法链接,可使用Gitea作为中转,先将代码提交道Gitea,然后Gitea配置自动推送到Github

设置头像

1
avatar: /images/avatar.jpg

头像地址如果是以/起始则表示头像图片放置在博客发布后的目录下,例如测试博客地址是http://localhost:4000,头像图片地址为http://localhost:4000/images/avatar.jpg
此配置需要在博客的source/images目录中放置头像图片avatar.jpg

侧边栏社交链接

1
2
3
4
5
6
7
8
9
10
social:
#GitHub: https://github.com/<username> || fab fa-github
#E-Mail: <email> || fa fa-envelope
#Weibo: https://weibo.com/yourname || fab fa-weibo
#Twitter: https://twitter.com/yourname || fab fa-twitter
#FB Page: https://www.facebook.com/yourname || fab fa-facebook
#StackOverflow: https://stackoverflow.com/yourname || fab fa-stack-overflow
#YouTube: https://youtube.com/yourname || fab fa-youtube
#Instagram: https://instagram.com/yourname || fab fa-instagram
#Skype: skype:yourname?call|chat || fab fa-skype

next主题默认支持的社交链接 ||符号后是链接的图标

使用已有配置放开注释即可,如果要添加默认不存在链接示例如下

1
2
social:
微信: https://wx.qq.com || weixin

注意: 图标对应的名称是FontAwesom图标的名称(不必带 fa- 前缀)

打赏功能

1
2
3
4
# Reward 
reward:
wechatpay: /images/custom/wechatpay.jpg
alipay: /images/custom/alipay.jpg

放开此部分注释并在source/images中放入收款码图片

站点建立时间

1
2
footer:
since: 2025

订阅微信公众号

1
2
3
4
5
# Wechat Subscriber 
wechat_subscriber:
enabled: true
qcode: /images/wechat-qcode.jpg
description: 欢迎您扫一扫上面的微信公众号,订阅我的博客!

放开此部分注释,并在source/images中放入公众号二维码

注意: 此功能需要NexT版本在5.0.1之后

设置动画

NexT 默认开启动画效果,效果使用 JavaScript 编写,因此需要等待 JavaScript 脚本完全加载完毕后才会显示内容。 如果您比较在乎速度,可以将设置此字段的值为 false 来关闭动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Use velocity to animate everything. 
motion:
enable: true
async: true
transition:
# Transition variants:
# fadeIn | fadeOut | flipXIn | flipXOut | flipYIn | flipYOut | flipBounceXIn | flipBounceXOut | flipBounceYIn | flipBounceYOut
# swoopIn | swoopOut | whirlIn | whirlOut | shrinkIn | shrinkOut | expandIn | expandOut
# bounceIn | bounceOut | bounceUpIn | bounceUpOut | bounceDownIn | bounceDownOut | bounceLeftIn | bounceLeftOut | bounceRightIn | bounceRightOut
# slideUpIn | slideUpOut | slideDownIn | slideDownOut | slideLeftIn | slideLeftOut | slideRightIn | slideRightOut
# slideUpBigIn | slideUpBigOut | slideDownBigIn | slideDownBigOut | slideLeftBigIn | slideLeftBigOut | slideRightBigIn | slideRightBigOut
# perspectiveUpIn | perspectiveUpOut | perspectiveDownIn | perspectiveDownOut | perspectiveLeftIn | perspectiveLeftOut | perspectiveRightIn | perspectiveRightOut
post_block: fadeIn
post_header: slideDownIn
post_body: slideDownIn
coll_header: slideLeftIn # Only for Pisces | Gemini.
sidebar: slideUpIn

设置全文阅读

在首页显示一篇文章的部分内容,并提供一个链接跳转到全文页面是一个常见的需求。 NexT 提供三种方式来控制文章在首页的显示方式。

  • 在文章中使用 <!-- more --> 手动进行截断,Hexo 提供的方式 推荐
  • 在文章的 front-matter 中添加 description,并提供文章摘录
  • 自动形成摘要,需要添加如下配置
    1
    2
    3
    4
    5
    # Automatically Excerpt. Not recommend. 
    # Please use <!-- more --> in the post to control excerpt accurately.
    auto_excerpt:
    enable: true
    length: 150

设置字数统计/阅读时长

_config.yml中配置如下

1
2
3
4
5
6
7
8
# Post wordcount display settings 
# Dependencies: https://github.com/willin/hexo-wordcount
post_wordcount:
item_text: true
wordcount: true
min2read: true
totalcount: false
separated_meta: true

加载进度条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Progress bar in the top during page loading.
pace: true
# Themes list:
#pace-theme-big-counter
#pace-theme-bounce
#pace-theme-barber-shop
#pace-theme-center-atom
#pace-theme-center-circle
#pace-theme-center-radar
#pace-theme-center-simple
#pace-theme-corner-indicator
#pace-theme-fill-left
#pace-theme-flash
#pace-theme-loading-bar
#pace-theme-mac-osx
#pace-theme-minimal
# For example
# pace_theme: pace-theme-center-simple
pace_theme: pace-theme-minimal

搜索服务

_config.yml中配置如下

1
2
3
4
5
6
# hexo-generator-searchdb 
search:
path: search.xml
field: post
format: html
limit: 10000

在_config.next.yml中配置如下

1
2
3
4
5
6
7
8
9
# Local search 
# Dependencies: https://github.com/flashlab/hexo-generator-search
local_search:
enable: true
# if auto, trigger search by changing input
# if manual, trigger search by pressing enter key or search button
trigger: auto
# show top n results per article, show all results by setting to -1
top_n_per_article: 1

参考
官方文档
Hexo 博客使用 Next 主题及美化 | Jiz4oh’s Life

前言

作为技术博主,博客的高效维护与部署一直是我关注的重点。近期在维护博客时,我遇到了两个核心问题:

  1. 内容管理混乱:草稿箱文件堆积,缺乏分类标准,甚至因误操作破坏了原有配置;
  2. 兼容性局限:计划将文章同步至 FastGPT 等 AI 知识库时,发现官方推荐的 Hexo 部署方案(源码与静态文件混存)中,冗余的 public 目录会干扰 RAG 系统提取内容,且源码与发布产物耦合易引发冲突。

为解决这些问题,我采用了源码与发布分离的部署架构:将 Markdown 源文件单独存放在一个仓库,通过 GitHub Actions 自动在另一个仓库构建并发布静态文件。这种方式的优劣对比如下:

方案 优点 缺点
官方混仓部署 支持本地手动 / 自动发布,预览方便,配置简单 仓库体积大,源码与产物混合,不利于二次利用
本文分离部署 源码纯净、产物独立,兼容 AI 知识库,自动构建 本地预览需搭测试环境,配置较复杂(双仓库 + 鉴权)

部署核心思路

核心逻辑:当源码仓库收到推送时,GitHub Actions 自动将源文件检出到 source/_posts,并从 _hexo 目录复制配置文件还原 Hexo 环境,最终执行构建与发布。

文件结构设计(源码仓库):

1
2
3
4
5
6
7
8
|-- _hexo/ # Hexo 核心配置目录 
| |-- _config.yml # Hexo 主配置
| |-- _config.next.yml # NexT 主题配置
| |-- package.json # Node 环境依赖
| |-- scaffolds/ # 文章模板(draft/page/post.md)
| |-- static/ # 静态资源(头像、支付码等)
|-- .github/workflows/ # GitHub Actions 工作流配置
|-- .obsidian/ # Obsidian 编辑器配置(可选)

详细部署步骤

1. 生成 SSH 密钥对(用于仓库间鉴权)

需要生成一对 SSH 密钥,用于源码仓库向发布仓库推送构建结果:

1
ssh-keygen -t rsa -C "<github 注册邮箱>"

执行后会在以下路径生成两个文件:

  • 私钥:~/.ssh/id_rsa(Linux/Mac)或 C:\Users\<用户名>\.ssh\id_rsa(Windows)
  • 公钥:~/.ssh/id_rsa.pub(同上路径)

注意:.ssh为隐藏目录,需要修改系统设置显示此文件夹

2. 准备两个仓库

仓库 1:源码仓库(存放 Markdown 与配置)

  • 新建仓库(例如命名为 hexo-source
  • 进入仓库设置:Settings → Secrets and variables → Actions → New repository secret
  • 添加一个名为 HEXO_DEPLOY_KEY 的密钥,值为私钥 id_rsa 的内容(用记事本打开复制)

仓库 2:发布仓库(存放静态文件,用于 GitHub Pages)

  • 仓库名必须为 <你的 GitHub 用户名>.github.io(固定格式,否则 GitHub Pages 无法生效)
  • 权限需设为公开,并开启 Discussions 功能(进入仓库设置 → Features 勾选)
  • 配置部署密钥:Settings → Deploy keys → Add deploy key
    • Title 填 HEXO_DEPLOY_PUB
    • Key 填入公钥 id_rsa.pub 的内容,并勾选 Allow write access(允许推送权限)

3. 配置 Hexo 环境文件

在源码仓库中创建 _hexo 目录,放入以下核心文件(可从本地 Hexo 环境中复制, 参考Hexo-博客配置):

  • _config.yml:Hexo 主配置(需修改部署相关配置,见步骤 4)
  • _config.next.yml:NexT 主题配置(其他主题同理)
  • package.json:依赖配置(需包含 hexohexo-deployer-git 等核心依赖)
  • scaffolds/:文章模板(draft.md/page.md/post.md
  • 静态资源:如头像(avatar.jpg)、关于页(about.md)等,按实际需求存放

4. 配置部署与工作流文件

① Hexo 部署配置(_hexo/_config.yml

在配置文件中添加部署规则,指向发布仓库:

1
2
3
4
5
6
7
8
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
repo: git@github.com:<username>/<username>.github.io.git
branch: master
name: <username>
email: <email>

② GitHub Actions 工作流(.github/workflows/hexo-deploy.yml

创建工作流文件,实现自动构建部署:

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
name: hexo-deploy  # 工作流名称

# 触发条件:向 master 分支推送时执行
on:
push:
branches: ["master"]

jobs:
build:
runs-on: ubuntu-latest # 使用 Ubuntu 环境
steps:
# 1. 配置时区(避免时间显示异常)
- name: Setup Timezone
uses: szenius/set-timezone@v2.0
with:
timezoneLinux: "Asia/Shanghai"

# 2. 拉取源码仓库内容到 source/_posts
- uses: actions/checkout@v3
with:
path: source/_posts

# 3. 安装 Node.js(需与本地开发环境版本一致,这里用 20.x)
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'

# 4. 缓存 NPM 依赖(加速构建)
- name: Cache NPM dependencies
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.OS }}-npm-cache
restore-keys: |
${{ runner.OS }}-npm-cache

# 5. 配置 SSH 密钥(用于向发布仓库推送)
- name: Setup Git
env:
ACTION_DEPLOY_KEY: ${{ secrets.HEXO_DEPLOY_KEY }} # 引用源码仓库的私钥
run: |
mkdir -p ~/.ssh/
echo "$ACTION_DEPLOY_KEY" > ~/.ssh/id_rsa
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa # 严格权限,否则 SSH 会拒绝使用
ssh-keyscan github.com >> ~/.ssh/known_hosts # 信任 GitHub 主机

# 6. 拉取主题(以 NexT 为例,其他主题修改仓库地址即可)
- name: Use Themes
uses: actions/checkout@v3
with:
repository: next-theme/hexo-theme-next
path: themes/next

# 7. 还原 Hexo 环境(从 _hexo 目录复制配置文件)
- name: Setup Hexo
run: |
npm install -g hexo-cli # 全局安装 Hexo 命令行工具
# 复制核心配置文件
cp source/_posts/_hexo/_config.yml .
cp source/_posts/_hexo/_config.next.yml .
cp source/_posts/_hexo/package.json .
# 复制文章模板
mkdir scaffolds
cp source/_posts/_hexo/scaffolds/* scaffolds/
# 复制静态页面(关于页、分类页等,按实际需求调整)
mkdir -p source/about source/categories source/tags source/images
cp source/_posts/_hexo/about.md source/about/index.md
cp source/_posts/_hexo/categories.md source/categories/index.md
cp source/_posts/_hexo/tags.md source/tags/index.md
cp source/_posts/_hexo/*.jpg source/images/ # 复制图片资源
# 安装依赖
npm install

# 8. 缓存部署目录(加速后续构建)
- name: Cache Deploy
uses: actions/cache@v4
with:
path: .deploy_git
key: ${{ runner.OS }}-deploy-cache
restore-keys: |
${{ runner.OS }}-deploy-cache

# 9. 构建并发布
- name: Deploy
run: |
cd .deploy_git && git pull # 拉取最新发布内容,避免冲突
cd ..
hexo clean # 清理缓存
hexo generate # 生成静态文件
hexo deploy # 部署到发布仓库

验证与使用

  1. 将上述文件提交到源码仓库的 master 分支,GitHub Actions 会自动触发工作流;
  2. 进入源码仓库的 Actions 标签页,查看工作流执行状态,若显示绿色对勾则部署成功;
  3. 访问 https://<你的用户名>.github.io,即可看到最新发布的博客。

注意事项

  1. 私钥 HEXO_DEPLOY_KEY 是敏感信息,切勿泄露或提交到仓库;
  2. 发布仓库名必须严格为 <用户名>.github.io,否则 GitHub Pages 无法正常访问;
  3. 若主题是自定义修改过的,建议将主题 fork 到自己的仓库,再在工作流中拉取自己的 fork 版本;
  4. 本地预览时,可在源码仓库中手动搭建 Hexo 环境(复制 _hexo 目录文件,执行 hexo server)。

参考

Hexo官方提供的Github Actions部署示例

由于没有自己的云服务器,所以我之前选择博客工具的时候排除了Typora、Wordpress…转而选择了 Hexo,但其实相较于前者,Hexo 的云端写作体验一直很糟糕。   随着近两年 CI/CD、DevOps 这些概念的流行,很多工具都火了起来,像 Jenkins、Github的好基友Travis等等,但这些都不太适用我们的情况,Jenkins 也需要自己的服务器,而TravisCI我也测试了一下,本来是适用的,也很方便,但是官方宣布后续不再免费,只赠送 10000 积分用完即止,开通付费版则要 69刀/月 [俺支持不起,倒不如整一个云服务器,大佬请随意!]。   但是我偶然了解到全球最大的同性交友网站丢出了一个重磅炸弹-Github Actions,我发现利用此功能可以完美解决 Hexo 静态博客自动部署的问题,并且免费版每月赠送2000分钟的时长,完美!

  本篇博文就来浅谈一下 Github Actions 的原理,以及使用他简单实现 Hexo 静态博客的自动部署(即每次我们 push 源代码后,自动生成静态文件,并上传到我们的仓库或者云存储中;Github 本身可以开启运行结果邮件通知功能,有条件的也可以设置 WebHooks 来进行通知。),好了那么话不多说,我们直接开始,欢迎大佬批评指正。

Github Actions

简介

  前面我们有提到 CI/CD、DevOps 这些名词,其实就是我们一般开发完成后,需要进行测试、打包、发布等操作,这些动作其实都是可以自动完成的,之前提到的 Jenkins 就可以做到,但是需要有自己的服务器。

  而 Github Actions 服务,就是用来帮助我们完成这些动作,他既可以使用自己的服务器也可以使用 Github 的服务器(支持多种环境与语言)。

使用

  Github Actions 和其他工具一样,通过脚本文件来进行一系列复杂的操作,他也有自己的语法规则-官方文档。

  由于很多操作在不同项目里面是类似的,完全可以共享。GitHub 注意到了这一点,想出了一个很妙的点子,允许开发者把每个操作写成独立的脚本文件,存放到代码仓库,使得其他开发者可以引用,官方市场、github/actions 仓库。

  其他基础使用,推荐查看阮一峰老师的介绍。

原理(个人了解)

  其实 Github Actions 就是当我们完成触发条件后(例如:push/pull等),Github 通过我们编写的脚本文件把应该在本地运行的命令,放到他的服务器(也可以设置自己的服务器)上自动运行,大大减少我们的操作。

  经过博主的测试,Github 提供的服务器上预装了多种语言及一些常见的运行环境等,所以我们编写脚本其实很简单,只需按语法要求添加以下固定内容即可:

  • 提供脚本基本信息
  • 指定运行环境、触发条件
  • 编写任务、步骤、动作

  添加这些内容后,当匹配触发条件时,Github 就会读取我们的脚本文件,在服务器上的指定环境中运行我们预先写好的任务、步骤、动作。

好了,经过这些简单的了解后,我们开始配置 Github Actions吧。

Hexo 静态博客自动部署

建立博客源代码仓库

  因为我们需要 Hexo 源代码才能生成静态文件,所以我们需要建立一个私有仓库来保存我们的源代码,当然如果你觉得麻烦也可以建立一个分支来保存,此处就不介绍了。

Ps: 如果您还不会搭建 Hexo 博客,可以参考本站之前的 Hexo 搭建教学。

  • 具体操作如图所示

  仓库建立后,我们可以先把自己的源代码通过 Git 提交上去,这里就不介绍了,也可以参考之前的博客搭建教学。

Ps: 如果碰到 Github 连线失败的情况,建议禁用代理 git config --global --unset http.proxy,或者直接使用 open ssh 进行连线推送。

Hexo 简单配置与介绍

Github 的链接形式

  Github 这种网站的代码仓库地址常见有三种形式,适用于不同的情况,下面简单介绍一下。

  • 普通链接,一般在使用账号密码登录后或者ssh传输时使用。

代码语言:javascript

AI代码解释

复制

1
2
3
// 这种地址可以直接在仓库中复制
https://github.com/pandaoh/biugle.git
git@github.com:pandaoh/biugle.git
  • 账号密码链接,这种适用于自己调用 Github 的数据或者当 Api 使用等情形。

代码语言:javascript

AI代码解释

复制

1
https://{username}:{password}@github.com/pandaoh/biugle.git
  • token 链接,在 Github Settings 中生成 token 后,可以直接放到仓库地址中,这样就可以直接访问有权限的仓库,方便我们自动部署。

代码语言:javascript

AI代码解释

复制

1
https://{token}@github.com/pandaoh/biugle.git
生成 Github Token

  了解完 Github 这些链接形式后,我们就可以开始配置了,因为我们决定使用 Token 这种链接形式来进行连线推送等操作,所以首先就是生成 Github Token

  • 打开我们自己的 Github Settings,选择 Developer settings => Personal access tokens

  • 生成 token 后,此信息只会展示一次,我们先保存下来,因安全问题后文我统一将此 token 称为 $GH_TOKEN
修改 config.yml

  大部分人之前应该都是在本地进行博客编写,所以连接 Github 的方式应该都是使用的 ssh,那么前面我们为了方便后续自动部署,需要把 config.yml 文件中的 deploy->repository->github 值改成 token url 的形式。

代码语言:javascript

AI代码解释

复制

1
2
3
4
5
6
deploy:
- type: git
repository:
github: https://{$GH_TOKEN}@github.com/pandaoh/pandaoh.github.io.git,master
...
// 注意此处的 {$GH_TOKEN} 请替换成我们之前生成的 token 内容,此仓库地址是我们博客静态文件最终存放的仓库地址,即搭建教学中开通 Github Pages 服务的那个仓库地址。

添加 GitHub Actions 脚本

  配置完 Hexo,我们开始编写 Github 的脚本文件,GitHub Actions 的配置文件叫做 workflow 文件,存放在源代码仓库的 .github/workflows 目录。   workflow 文件采用 YAML 格式,文件名可以任意取,但是后缀名统一为 .yml,比如 test.yml。一个库可以有多个 workflow 文件。   GitHub 在我们完成预设触发条件时,只要发现 .github/workflows 目录里面有 .yml 文件,就会自动读取运行该文件。

添加步骤
  • 我们可以直接手动建立此文件,或者通过源代码仓库点击 Actions => 选择 Setup Node创建,但最终同样都需 push 到远端源代码仓库中。

参数介绍

建议阅读完前面给出的官方文档再来进行此处的了解 ^_^

  • 建立文件后,我们修改其配置如下。

代码语言:javascript

AI代码解释

复制

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
name: DoubleAm's Blog CI/CD # 脚本 workflow 名称

on:
push:
branches: [main, master] # 当监测 main,master 的 push
paths: # 监测所有 source 目录下的文件变动,所有 yml,json 后缀文件的变动。
- '*.json'
- '**.yml'
- '**/source/**'

jobs:
blog: # 任务名称
timeout-minutes: 30 # 设置 30 分钟超时
runs-on: ubuntu-latest # 指定最新 ubuntu 系统
steps:
- uses: actions/checkout@v2 # 拉取仓库代码
- uses: actions/setup-node@v2 # 设置 node.js 环境
- name: Cache node_modules # 缓存 node_modules,提高编译速度,毕竟每月只有 2000 分钟。
uses: actions/cache@v2 # 亲测 Github 服务器编译速度比我自己电脑都快,如果每次构建按5分钟计算,我们每个月可以免费部署 400 次,Github yyds!!!
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Init Node.js # 安装源代码所需插件
run: |
npm install
echo "init node successful"
- name: Install Hexo-cli # 安装 Hexo
run: |
npm install -g hexo-cli --save
echo "install hexo successful"
- name: Build Blog # 编译创建静态博客文件
run: |
hexo clean
hexo g
echo "build blog successful"
- name: Deploy DoubleAm's Blog # 设置 git 信息并推送静态博客文件
run: |
git config --global user.name "doubleam"
git config --global user.email "admin@biugle.cn"
hexo deploy

- run: echo "Deploy Successful!"

验证结果

Hexo 与 Github Actions 均配置完成后,我们将这两个文件变动都推送至源代码仓库中。

推送内容

完成以上操作后,我们每修改并 push 一次监测的文件,就可以触发脚本运行。

查看 Github Actions 运行日志与结果

触发后我们可以查看运行日志与结果,如下图所示。

  • 基础日志

  • 详细日志

添加 WebHooks 通知

  脚本运行完成一般都有邮件通知,但如果我们需要在 push 后添加其他通知,例如钉钉机器人、QQ等,我们可以添加 WebHooks 来进行通知。

  • 可以直接在仓库添加 WebHooks,选择触发条件后,输入接口地址【POST】与 Secret (可选),Github 会在我们完成触发条件时,携带此次操作的信息数据包请求一次这个 POST 接口,至于后面的处理就可以自定义啦。

本文是一份关于 Locust 性能测试工具的全面配置与运行指南,涵盖环境变量、配置文件、命令行参数、无头模式、分布式运行以及自定义扩展等核心内容。

一、 配置方式总览

Locust 支持多种配置方式,优先级从低到高依次为:

  1. ~/locust.conf (用户主目录)
  2. ./locust.conf (当前目录)
  3. --config 参数指定的文件
  4. 环境变量 (格式:LOCUST_<OPTION_NAME>)
  5. 命令行参数 (最高优先级)
1.1 通过环境变量配置
1
2
3
4
5
6
# Linux/macOS
LOCUST_LOCUSTFILE=custom_locustfile.py locust

# Windows (cmd)
set LOCUST_LOCUSTFILE=custom_locustfile.py
locust
1.2 通过配置文件配置

创建 locust.confmaster.conf 文件:

1
2
3
4
5
6
7
8
9
# master.conf 示例
locustfile = locust_files/my_locust_file.py
headless = true
master = true
expect-workers = 5
host = http://target-system
users = 100
spawn-rate = 10
run-time = 10m

使用配置文件运行:

1
locust --config=master.conf

二、 核心运行模式

2.1 基础命令与常用选项
命令行参数 环境变量 描述
-f, --locustfile LOCUST_LOCUSTFILE 指定 Locust 测试脚本文件或目录。
-H, --host LOCUST_HOST 设置待测试系统的基准 URL。
-u, --users LOCUST_USERS 模拟的最大并发用户数。
-r, --spawn-rate LOCUST_SPAWN_RATE 每秒孵化的用户数。
--web-host, --web-port -P LOCUST_WEB_HOST, LOCUST_WEB_PORT 指定 Web UI 绑定的主机和端口。
2.2 无头模式运行 (Headless)

禁用 Web UI,直接通过命令行控制测试。

1
locust -f locustfile.py --headless -u 100 -r 5
  • 设置运行时间:使用 -t, --run-time 参数(例如 1h30m, 300s)。
  • 允许任务完成:使用 -s, --stop-timeout 参数,在停止前等待任务完成迭代(例如 --stop-timeout 10s)。
  • 动态调整用户数:即使在无头模式下,也可按 w/W(增加1/10用户)或 s/S(减少1/10用户)实时调整。
2.3 分布式运行

**主节点 (Master)**:负责协调和收集数据。

1
locust -f locustfile.py --master --expect-workers 3
  • --expect-workers:指定期望连接的工作节点数,连接齐备后才开始测试。
  • --master-bind-host/--master-bind-port:绑定主节点的接口和端口(默认 *:5557)。

**工作节点 (Worker)**:执行实际的负载生成。

1
locust -f locustfile.py --worker --master-host=192.168.1.100
  • --master-host/--master-port:指向主节点的地址。

无头模式下的分布式:主节点必须指定 --expect-workers 以等待工作节点就绪。

2.4 使用多个 Locustfile
  1. 指定目录:Locust 会递归扫描目录下的 .py 文件(忽略 locust.py 和以 _ 开头的文件)。
    1
    locust -f locustfiles/
  2. 指定多个文件:用逗号分隔。
    1
    locust -f locustfile1.py,locustfile2.py,locustfile3.py
2.5 用户类选择器 (Class Picker)

使用 --class-picker 参数启动,可在 Web UI 中选择本次运行要使用的特定 User 类或 Shape 类,而不是运行所有类。

1
locust -f locustfiles/ --class-picker

Web UI Class Picker

三、 高级功能与定制

3.1 自定义命令行参数

通过 init_command_line_parser 事件钩子添加自定义参数,这些参数可自动同步到工作节点并在 Web UI 中显示。

1
2
3
4
5
6
7
8
9
10
11
from locust import events

@events.init_command_line_parser.add_listener
def _(parser):
parser.add_argument("--my-argument", type=str, env_var="LOCUST_MY_ARGUMENT", default="", help="自定义参数说明")
# 设置 include_in_web_ui=False 可在 Web UI 中隐藏此参数
parser.add_argument("--hidden-arg", include_in_web_ui=False, default="invisible")

@events.test_start.add_listener
def _(environment, **kw):
print(f"自定义参数值: {environment.parsed_options.my_argument}")
3.2 定制统计信息设置

通过修改 locust.stats 模块的常量来调整统计行为。

1
2
3
4
5
6
7
8
import locust.stats

# 调整控制台输出间隔为15秒
locust.stats.CONSOLE_STATS_INTERVAL_SEC = 15
# 调整 CSV 文件写入间隔为5秒
locust.stats.CSV_STATS_INTERVAL_SEC = 5
# 调整要报告的百分位数
locust.stats.PERCENTILES_TO_REPORT = [0.5, 0.95, 0.99]

可定制参数

  • STATS_NAME_WIDTH:控制台输出中请求名称列的宽度。
  • CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW:计算当前响应时间百分位数的窗口大小(秒)。
3.3 自定义退出条件与退出码

通过 quitting 事件监听器,可以基于测试结果(如失败率、响应时间)自定义进程退出码,便于集成到 CI/CD 流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
from locust import events
import logging

@events.quitting.add_listener
def _(environment, **kw):
if environment.stats.total.fail_ratio > 0.01:
logging.error("失败率超过1%")
environment.process_exit_code = 1
elif environment.stats.total.avg_response_time > 200:
logging.error("平均响应时间超过200ms")
environment.process_exit_code = 1
else:
environment.process_exit_code = 0

注意:默认情况下,如果有任何请求失败,Locust 会以退出码 1 结束。可使用 --exit-code-on-error 参数改变此行为。

四、 其他实用配置选项速查

命令行参数 环境变量 用途简述
--autostart LOCUST_AUTOSTART 自动开始测试(保留 Web UI)。
--autoquit LOCUST_AUTOQUIT 测试结束后自动退出(需与 --autostart 同用)。
--web-auth LOCUST_WEB_AUTH 为 Web UI 设置基础认证(格式:username:password)。
-T, --tags LOCUST_TAGS 仅执行带有指定标签的任务。
-E, --exclude-tags LOCUST_EXCLUDE_TAGS 排除带有指定标签的任务。
--csv LOCUST_CSV 将统计数据保存为 CSV 文件(生成 *_stats.csv, *_stats_history.csv, *_failures.csv)。
--html LOCUST_HTML 将 HTML 报告保存到指定文件。
--print-stats LOCUST_PRINT_STATS 在控制台定期打印统计信息。
--only-summary LOCUST_ONLY_SUMMARY 在无头模式下只打印最终摘要,禁止定期打印。
--loglevel -L LOCUST_LOGLEVEL 设置日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)。
--logfile LOCUST_LOGFILE 指定日志文件路径。

一、核心原理与前置条件

iptables 端口转发(也称为 DNAT/SNAT)是在 Linux 内核层面进行的网络数据包重定向。它比应用层转发(如 SSH 隧道)效率更高,因为它直接修改数据包的 IP 头和端口信息。

1.1 开启系统路由转发功能

这是实现跨网段端口转发(尤其是转发到其他主机)的必备前提。系统默认禁止转发数据包。

临时开启(重启失效):

1
2
3
echo 1 > /proc/sys/net/ipv4/ip_forward
# 或
sysctl -w net.ipv4.ip_forward=1

永久开启:
编辑 /etc/sysctl.conf 文件,添加或修改以下行:

1
net.ipv4.ip_forward = 1

然后执行 sysctl -p 使配置立即生效。

1.2 理解关键链与表

iptables 的 nat 表是实现端口转发的核心,主要涉及两个链:

  • PREROUTING:数据包进入本机、经过路由判断之前。在此链进行 DNAT(目标地址转换),即修改数据包的“目标IP:端口”。
  • POSTROUTING:数据包经过路由判断、即将发送出网卡之前。在此链进行 SNAT(源地址转换),即修改数据包的“源IP”,确保回包能正确返回。

简单记忆PREROUTING 改“去哪”,POSTROUTING 改“从哪来”。


二、实战场景与配置命令

2.1 场景一:本机端口转发(本地转发)

将发往本机 A 端口的数据,转发到本机的 B 端口。

示例:将本机 7777 端口转发到 6666 端口。

1
2
3
4
5
# 外部访问本机时生效
iptables -t nat -A PREROUTING -p tcp --dport 7777 -j REDIRECT --to-port 6666

# 本机自己访问自己时生效(如 localhost:7777)
iptables -t nat -A OUTPUT -p tcp --dport 7777 -j REDIRECT --to-port 6666

说明

  • -t nat:指定操作 nat 表。
  • -A PREROUTING:在 PREROUTING 链末尾追加规则。
  • -p tcp:匹配 TCP 协议。
  • --dport 7777:匹配目标端口为 7777。
  • -j REDIRECT:执行重定向动作。
  • --to-port 6666:重定向到端口 6666。

2.2 场景二:转发到另一台主机(最常用)

将发往本机(转发机) A 端口的数据,转发到另一台内网/外网主机 BC 端口。

网络拓扑

  • **转发机 (FW)**:IP 192.168.1.168,接收来自客户端的 6666 端口请求。
  • **目标机 (TARGET)**:IP 192.168.1.8,实际提供服务在 7777 端口。

在转发机 (FW) 上执行:

1
2
3
4
5
6
7
8
# 1. 开启路由转发(必须)
sysctl -w net.ipv4.ip_forward=1

# 2. DNAT:修改目标地址 (PREROUTING链)
iptables -t nat -A PREROUTING -p tcp --dport 6666 -j DNAT --to-destination 192.168.1.8:7777

# 3. SNAT:修改源地址,确保回包能经过本机 (POSTROUTING链)
iptables -t nat -A POSTROUTING -p tcp -d 192.168.1.8 --dport 7777 -j SNAT --to-source 192.168.1.168

命令解析

  • -j DNAT --to-destination:执行目标地址转换。
  • -j SNAT --to-source:执行源地址转换,--to-source 通常应设置为转发机的内网IP。如果转发机有公网IP且用于转发公网流量,这里也应填内网IP

2.3 场景三:公网IP主机间的转发

当两台主机都有公网IP时,转发规则与场景二类似,但 SNAT --to-source 必须设置为转发机的内网IP(私网IP),而非公网IP。

错误示例(会导致转发失败):

1
iptables -t nat -A POSTROUTING -p tcp -d <目标机公网IP> --dport XXXX -j SNAT --to-source <转发机公网IP> # 错误!

数据包进入主机后,其目的地址已是内网IP。SNAT 为公网IP会导致回包路径异常。

2.4 场景四:UDP 端口转发

UDP 转发与 TCP 转发命令结构相同,只需将协议 -p tcp 改为 -p udp

UDP 转发完整示例(将本机 100 端口转发到 192.168.1.1:80):

1
2
3
4
5
6
7
# DNAT
iptables -t nat -A PREROUTING -d 10.10.10.1 -p udp --dport 100 -j DNAT --to-destination 192.168.1.1:80
# SNAT (注意参数顺序与TCP示例略有不同,但原理一致)
iptables -t nat -A POSTROUTING -s 192.168.1.1 -p udp --dport 80 -j SNAT --to-source 10.10.10.1:100
# 允许转发 (FORWARD链)
iptables -A FORWARD -o eth0 -d 192.168.1.1 -p udp --dport 80 -j ACCEPT
iptables -A FORWARD -i eth0 -s 192.168.1.1 -p udp --sport 80 -j ACCEPT

三、规则管理与持久化

3.1 查看规则

1
2
3
4
# 查看 nat 表的所有规则(带行号)
iptables -t nat -nL --line-numbers
# 查看 filter 表的 FORWARD 链规则
iptables -L FORWARD --line-numbers

3.2 删除规则

通过规则序号删除是最高效的方式。

1
2
3
4
# 删除 nat 表 PREROUTING 链的第 1 条规则
iptables -t nat -D PREROUTING 1
# 删除 filter 表 FORWARD 链的第 2 条规则
iptables -D FORWARD 2

3.3 保存规则(永久生效)

配置的规则默认在重启后丢失,需要保存。

  • CentOS 6 / RHEL 6
    1
    service iptables save
    规则将保存到 /etc/sysconfig/iptables
  • CentOS 7 / RHEL 7
    首先安装 iptables-services
    1
    2
    3
    yum install iptables-services
    systemctl enable iptables
    systemctl start iptables
    然后使用相同命令保存:service iptables saveiptables-save > /etc/sysconfig/iptables

3.4 清空与重置

1
2
3
4
5
6
7
8
# 清除 nat 表所有链的规则
iptables -t nat -F
# 清除 filter 表所有链的规则
iptables -F
# 清除所有用户自定义链
iptables -X
# 计数器归零
iptables -Z

警告:在生产环境谨慎使用 -F-X,可能导致网络中断。


四、综合示例与排错

4.1 完整示例:实现内网穿透式访问

目标:让客户端能通过转发机 A 访问到后端服务器 B 的 Web 服务。

  • **转发机 (A)**:IP 1.1.1.1(公网),2.2.2.2(转发机内网IP,用于SNAT)
  • **后端服务器 (B)**:IP 192.168.1.100(内网),运行在 80 端口。

在转发机 A 上配置:

1
2
3
4
5
6
7
8
9
# 开启路由转发
echo 1 > /proc/sys/net/ipv4/ip_forward

# 添加转发规则
iptables -t nat -A PREROUTING -p tcp --dport 999 -j DNAT --to-destination 192.168.1.100:80
iptables -t nat -A POSTROUTING -d 192.168.1.100 -p tcp --dport 80 -j SNAT --to-source 2.2.2.2

# 保存规则
service iptables save

结果:客户端访问 http://1.1.1.1:999,实际获取到的是 http://192.168.1.100:80 的内容。

4.2 常见问题与排查

  1. 转发不生效

    • 检查是否已开启 net.ipv4.ip_forward = 1
    • 检查 iptables -t nat -nL 规则是否添加成功。
    • 检查目标服务器的防火墙是否放行了来自转发机 IP 的流量。
    • 使用 tcpdump 在转发机抓包,观察数据包是否被正确修改和转发。
      1
      tcpdump -i any port [源端口] or [目标端口] -nn
  2. 本机无法访问自己的转发端口
    确保添加了 OUTPUT 链的 REDIRECT 规则(见场景一)。

  3. CentOS 7 没有 service iptables save 命令
    安装 iptables-services 包。


五、总结

iptables 端口转发是一项强大而高效的内核级网络功能。掌握其核心步骤:

  1. 开启路由转发 (ip_forward=1)。
  2. 添加 DNAT 规则 (PREROUTING 链),修改“去哪”。
  3. 添加 SNAT 规则 (POSTROUTING 链),修改“从哪来”,确保回包路径正确。
  4. 保存规则,确保重启后生效。

对于简单的本机端口重定向,使用 REDIRECT 动作更为便捷。对于复杂的网络环境,理解 DNAT/SNAT 的原理和 PREROUTING/POSTROUTING 链的工作时机是成功配置的关键。

官方文档(中文)

节点名称 节点IP 配置 系统版本
VIP 192.168.50.220 虚拟IP
k8s-master-221 192.168.50.221 4核 2G debian 11
k8s-master-222 192.168.50.222 4核 2G debian 11
k8s-master-223 192.168.50.223 4核 2G debian 11
k8s-node-224 192.168.50.224 4核 2G debian 11
k8s-node-225 192.168.50.225 4核 2G debian 11

主机配置

时间同步

1

配置 hostname

注意节名称不能重复

1
hostnamectl --static set-hostname k8s-master-221

配置防火墙

1
2
3
4
5
service iptables stop 

iptables -F

systemctl stop firewalld && systemctl disable firewalld

如果需要打开防火墙,执行以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# master节点执行
ufw allow 6443/tcp
ufw allow 2379/tcp
ufw allow 2380/tcp
ufw allow 10250/tcp
ufw allow 10251/tcp
ufw allow 10252/tcp
ufw allow 10255/tcp
ufw reload

# worker节点执行
ufw allow 10250/tcp
ufw allow 30000:32767/tcp
ufw reload

关闭交换分区

1
2
swapoff -a
set -ri 's/.*swap.*/#&/' /etc/fstab

若需允许交换分区参考官方文档 交换分区的配置

配置hosts

1
2
3
4
5
6
7
cat >> /etc/hosts << EOF
192.168.50.221 k8s-master-221
192.168.50.222 k8s-master-222
192.168.50.223 k8s-master-223
192.168.50.224 k8s-worker-224
192.168.50.225 k8s-worker-225
EOF

开启 bridge 网桥过滤功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 桥接的ipv4流量转到iptables
cat << EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

# 设置所需的 sysctl 参数,参数在重新启动后保持不变
cat << EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1 # 开启网桥模式(必须)
net.bridge.bridge-nf-call-ip6tables = 1 # 开启网桥模式(必须)
net.ipv4.ip_forward = 1 # 转发模式(默认开启)
vm.panic_on_oom = 0 # 开启OOM(默认开启)
vm.swappiness  = 0 # 禁止使用swap空间
vm.overcommit_memory = 1 # 不检查物理内存是否够用
EOF

# 应用 sysctl 参数而不重新启动
sudo sysctl --system

配置 IPVS

1
2
3
4
5
6
7
8
9
10
11
modprobe br_netfilter

cat > /etc/sysconfig/modules/ipvs.modules << EOF
#!/bin/bash
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv
EOF

安装工具

安装 Containerd

1
2
3
4
5
6
7
# 安装
apt update
apt install -y containerd

# 导出默认配置
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null 2>&1

设置cgroupdriversystemd,编辑 /etc/containerd/config.toml 文件,找到 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] 部分,添加一行内容:SystemdCgroup = true

1
sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml

重启containerd并设置开机启动

1
2
systemctl restart containerd
systemctl enable containerd

安装 keadm,kubelete,kubectl

1
2
3
4
5
6
# 添加安装源

# 安装
apt update
apt install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

部署高可用(仅 master 节点)

安装

1
apt install keepalived haproxy

修改haproxy配置

/etc/haproxy/haproxy.cfg

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
global
maxconn 2000
ulimit-n 16384
log 127.0.0.1 local0 err
stats timeout 30s

defaults
log global
mode http
option httplog
timeout connect 5000
timeout client 50000
timeout server 50000
timeout http-request 15s
timeout http-keep-alive 15s

frontend monitor-in
bind *:33305
mode http
option httplog
monitor-uri /monitor

frontend k8s-master
bind 0.0.0.0:16443
bind 127.0.0.1:16443
mode tcp
option tcplog
tcp-request inspect-delay 5s
default_backend k8s-master

backend k8s-master
mode tcp
option tcplog
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100
server k8s-master1 172.16.12.111:6443 check
server k8s-master2 172.16.12.112:6443 check
server k8s-master3 172.16.12.113:6443 check

配置 keepalived

interface # 网卡名称
mcast_src_ip # 节点ip
virtual_ipaddress # vip地址

k8s-master-221配置文件/etc/keepalived/keepalived.conf

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
! Configuration File for keepalived
global_defs {
router_id LVS_DEVEL
script_user root
enable_script_security
}
vrrp_script chk_apiserver {
script "/etc/keepalived/check_apiserver.sh" #健康检查脚本
interval 5
weight -5
fall 2
rise 1
}
vrrp_instance VI_1 {
state MASTER #高可用主1
interface eth0 #网卡名称
mcast_src_ip 192.168.50.221 #该节点 IP
virtual_router_id 51
priority 100 #设置最高级优先级
advert_int 2
authentication {
auth_type PASS
auth_pass K8SHA_KA_AUTH
}
virtual_ipaddress {
192.168.50.220 #vip地址
}
track_script {
chk_apiserver
}
}

k8s-master-222配置文件/etc/keepalived/keepalived.conf

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
! Configuration File for keepalived
global_defs {
router_id LVS_DEVEL
script_user root
enable_script_security
}
vrrp_script chk_apiserver {
script "/etc/keepalived/check_apiserver.sh"
interval 5
weight -5
fall 2
rise 1
}
vrrp_instance VI_1 {
state BACKUP #高可用 从1
interface ens33 #网卡名称
mcast_src_ip 192.168.50.222 #该节点 IP
virtual_router_id 51
priority 50 #设置优先级
advert_int 2
authentication {
auth_type PASS
auth_pass K8SHA_KA_AUTH
}
virtual_ipaddress {
192.168.50.220 #vip地址
}
track_script {
chk_apiserver
}
}

k8s-master-222配置文件/etc/keepalived/keepalived.conf

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
! Configuration File for keepalived
global_defs {
router_id LVS_DEVEL
script_user root
enable_script_security
}
vrrp_script chk_apiserver {
script "/etc/keepalived/check_apiserver.sh"
interval 5
weight -5
fall 2
rise 1
}
vrrp_instance VI_1 {
state BACKUP #高可用从2
interface ens33 #网卡名称
mcast_src_ip 192.168.50.223 #该节点 IP
virtual_router_id 51
priority 49 #设置优先级
advert_int 2
authentication {
auth_type PASS
auth_pass K8SHA_KA_AUTH
}
virtual_ipaddress {
192.168.50.220 #vip地址
}
track_script {
chk_apiserver
}
}

健康检查脚本 /etc/keepalived/check_apiserver.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
err=0
for k in $(seq 1 3);do
check_code=$(pgrep haproxy)
if [[ $check_code == "" ]]; then
err=$(expr $err + 1)
sleep 1
continue
else
err=0
break
fi
done

if [[ $err != "0" ]]; then
echo "systemctl stop keepalived"
/usr/bin/systemctl stop keepalived
exit 1
else
exit 0
fi

给监测脚本添加执行权限

1
chmod +x /etc/keepalived/check_apiserver.sh

启动keepalive和haproxy

1
2
3
4
5
6
systemctl daemon-reload
# 启动并设置开机启动
# systemctl enable --now haproxy
systemctl start haproxy && systemctl enable haproxy
# systemctl enable --now keepalived
systemctl start keepalived && systemctl enbale keepalived

测试vip漂移

1
2
3
4
5
# 查看ip与vip
hostname -I

# 测试vip的16443端口是否通
nc -v 192.168.50.220 16443

初始化集群

拉取镜像

1
2
3
4
5
# 查看需要的镜像文件
kubeadm config images list

# 拉取镜像
kubeadm config images pull

master 节点初始化

1
2
3
4
5
# 导出默认初始化配置
kubeadm config print init-defaults > kubeadm-config.yaml

# token过期后生成信息token
kubeadm token create --print-join-command

master 节点加入集群

1
2
3
4
5
6
# master节点需要生成certificate-key
kubeadm init --control-plane-endpoint=192.168.50.220:16443

kubeadm join 192.168.50.220:16443 --token {token} \
--discovery-token-ca-cert-hash {} \
--control-plane --certificate-key {}

worker 节点加入集群

1
2
kubeadm join 192.168.50.220:16643 --token {token} \
--discovery-token-ca-cert-hash {}

从集群种移除节点

1
kubectl delete node {node-name}

配置环境变量,用于访问集群

1
2
3
4
5
cat << EOF >> ~/.bashrc
export KUBECONFIG=/etc/kubernetes/admin/conf
EOF

source ~/.bashrc

查看集群节点状态

1
2
3
4
5
6
# 查看节点状态
kubectl get nodes

# 查看系统组件
kubectl get all -n kube-system -o wide

安装网络组件(只在master-221节点操作)

Calico
Flannel

去除 master节点污点

如果你打算让Master节点也参与到平常的Pod调度(生产环境一般不会这样做,以保证master节点的稳定性),那么你需要使用以下命令将Master节点上的 taint(污点标记)解除

1
kubectl taint nodes --all node-role.kubernetes.io/master-

最后我们使用以下命令查看当前集群的状态,发现Scheduler和Controller Manager组件处理不健康状态:

1
kubectl get cs

解决上述问题需要将每个Master节点上的 /etc/kubernetes/manifests/kube-scheduler.yaml 和 /etc/kubernetes/manifests/kube-controller-manager.yaml 文件中的- –port=0注释掉,然后重启一下各Master节点上的kubelet即可.

测试集群

1
2
3
4
kubectl create deployment nginx --image nginx --replicas 2
kubectl expose deployment nginx --name nginx --type NodePort --port 80 --target-port 80 --node-port 8080

curl http://192.168.50.220:8080

参考
如何用 Kubeadm 在 Debian 11 上安装 Kubernetes 集群 | Linux 中国 - 知乎 (zhihu.com)
Kubernetes多主多从高可用集群部署 - 个人文章 - SegmentFault 思否
搭建多主节点k8s高可用集群(三主两从一VIP)_kubernetes部署多主多从集群-CSDN博客
github - 基于Ubuntu22.04部署KubeEdge-v1.18.0环境 - 云原生_KubeEdge - SegmentFault 思否

概述

Bitwarden 是一款开源免费的密码管理器,支持不限数量的密码存储、跨平台同步和自建服务器部署。核心优势包括完全免费、功能完整、开源透明。


一、核心功能

1.1 密码生成与管理

  • 多类型存储:账号密码、TOTP两步验证、自定义笔记/字段
  • 智能分类:一级文件夹结构(不支持嵌套)
  • 高级密码生成
    • 长度:最长128位
    • 自定义:数字/特殊字符最小数量
    • 可视化:不同字符类型彩色显示
    • 密码短语:最多20个单词,支持分隔符/大小写/数字配置
    • 历史记录:保存所有生成过的密码

1.2 数据迁移

  • 导入支持:Chrome、Firefox、1Password、LastPass等主流管理器
  • 导出格式:JSON、CSV(便于备份和迁移)

1.3 安全报告

  • 泄露检测:对比公开泄露数据库
  • 重复使用检测:标记重复密码
  • 弱密码识别:发现易破解密码
  • HTTPS检查:提醒非HTTPS域名
  • 2FA建议:提示未启用两步验证的网站

1.4 账户保护

  • 指纹短语:5个固定英文单词组成的唯一标识(用于验证服务器/组织成员)
  • KDF强化:增加主密码暴力破解难度
  • 基础防护:后台自动锁定、禁止截图

二、自建部署

2.1 环境准备

CentOS

1
2
3
4
5
6
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io
sudo systemctl enable --now docker
sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Ubuntu

1
2
3
4
5
6
sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose
sudo systemctl enable --now docker

2.2 部署方案

官方镜像

  1. Bitwarden官网 申请 installation idkey
  2. 执行安装脚本:
    1
    2
    3
    curl -Lso bitwarden.sh https://go.btwrdn.co/bw-sh && chmod +x bitwarden.sh
    ./bitwarden.sh install # 按提示输入域名和密钥
    ./bitwarden.sh start

第三方镜像(推荐)

使用 bitwarden_rs(Rust重写版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 拉取镜像
docker pull bitwardenrs/server:latest

# 测试运行
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 bitwardenrs/server:latest

# 生产配置(关键参数)
docker run -d --name bitwarden \
-v /bw-data/:/data/ \
-e SIGNUPS_ALLOWED=false \ # 禁止注册(先注册账号!)
-e ADMIN_TOKEN=复杂随机字符串 \ # 启用管理页面
-e SHOW_PASSWORD_HINT=false \ # 隐藏密码提示
-e WEBSOCKET_ENABLED=true \ # 启用通知
-e DOMAIN=https://your-domain.com \ # 必须设置域名
-p 80:80 -p 3012:3012 \ # 通知端口
bitwardenrs/server:latest

2.3 Nginx反向代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
server_name your-domain.com;
client_max_body_size 128M;

location / {
proxy_pass http://localhost:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /notifications/hub {
proxy_pass http://localhost:3012;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

location /notifications/hub/negotiate {
proxy_pass http://localhost:80;
}

listen 80;
}

注意:生产环境需配置SSL证书(推荐certbot自动配置)


三、安全最佳实践

  1. 首次部署:先注册个人账号再禁用注册(SIGNUPS_ALLOWED=false
  2. 管理页面:设置高强度ADMIN_TOKEN并启用HTTPS
  3. 邮件服务(可选):
    1
    2
    3
    4
    5
    6
    -e SMTP_HOST=smtp.domain.tld \
    -e SMTP_FROM=bitwarden@domain.tld \
    -e SMTP_PORT=587 \
    -e SMTP_SSL=true \
    -e SMTP_USERNAME=username \
    -e SMTP_PASSWORD=password

总结

Bitwarden 通过开源免费、功能完整和自建支持三大优势,成为个人密码管理的理想选择。自建部署时推荐使用第三方Rust镜像,配合Nginx反向代理和严格的安全配置,可兼顾便利性与数据隐私。

本文是一篇实战教程,详细记录了如何将 vue-admin-template 项目从使用前端 Mock 数据,切换为对接真实的、基于 JWT 认证的后端 API(本例后端为 FastAPI)。涵盖环境配置、请求封装、登录认证、用户信息获取及页面路由调整等核心环节。

一、 概述与目标

项目背景vue-admin-template 是一个极简的 Vue.js 后台管理基础模板,默认使用前端 Mock 数据模拟接口。

对接目标

  1. 配置前端请求指向真实后端地址。
  2. 实现基于 JWT Token 的登录认证流程。
  3. 修改用户信息获取逻辑,匹配后端数据结构。
  4. 调整前端页面(如个人中心)和路由。

最终效果:前端能成功调用后端 API 完成登录、获取数据,并实现完整的权限控制流程。

后台效果演示

二、 核心对接步骤

1. 配置后端 API 基础地址

前端通过环境变量 VUE_APP_BASE_API 控制请求的基础 URL。

  1. 打开项目根目录下的环境配置文件 .env.development(开发环境)。
  2. 修改变量值为你的后端 API 地址:
    1
    2
    # 例如,后端服务运行在本地 8010 端口
    VUE_APP_BASE_API = 'http://127.0.0.1:8010/api/admin/v1'
    生产环境配置在 .env.production 文件中。
2. 修改登录认证逻辑

(1)调整登录接口 (src/api/user.js)
找到 login 函数,修改 urlmethod 以匹配后端登录接口。

1
2
3
4
5
6
7
export function login(data) {
return request({
url: '/auth/login/access-token', // 根据后端接口修改
method: 'post',
data // 通常包含 username/email 和 password
})
}

(2)修改账号验证规则 (src/utils/validate.js)
默认模板验证用户名只能是 admineditor。需要改为验证邮箱(或其他你的登录凭证)。

1
2
3
4
5
6
7
8
9
10
11
/**
* 验证邮箱格式
* @param {string} str
* @returns {Boolean}
*/
export function validEmail(str) {
const emailReg = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
return emailReg.test(str)
}

// 在 `src/views/login/index.vue` 中,将 `validUsername` 的调用替换为 `validEmail`

(3)调整 Axios 请求拦截器 (src/utils/request.js)
后端通常要求在请求头中携带 Token 进行认证。

1
2
3
4
5
6
7
8
9
10
11
12
13
// request interceptor
service.interceptors.request.use(
config => {
if (store.getters.token) {
// 根据后端约定修改 header 字段名,例如 'Authorization' 或 'token'
config.headers['token'] = getToken() // 将默认的 'X-Token' 改为 'token'
}
return config
},
error => {
return Promise.reject(error)
}
)

(4)调整 Axios 响应拦截器 (src/utils/request.js)
根据后端统一的响应格式修改成功/失败的判断逻辑。假设后端成功时 code200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
service.interceptors.response.use(
response => {
const res = response.data

// 判断请求是否成功(根据后端返回的 code)
if (res.code !== 200) { // 将默认的 20000 改为 200
// 处理错误...
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res // 直接返回 res,其中包含 data, code, message
}
},
error => {
// 处理 HTTP 错误...
return Promise.reject(error)
}
)
3. 处理用户信息

登录成功后,前端会请求用户信息。需要修改 Vuex 中的处理逻辑以匹配后端返回的数据结构。

**修改 src/store/modules/user.js**:
getInfo action 中,根据后端返回的字段名进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 假设后端返回数据格式: { code:200, data: { nickname: 'xxx', avatar: 'xxx' } }
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response

if (!data) {
reject('Verification failed, please Login again.')
}

const { nickname, avatar } = data // 解构出后端返回的字段

commit('SET_NAME', nickname) // 修改字段映射
commit('SET_AVATAR', avatar)
resolve(data)
}).catch(error => {
reject(error)
})
})
}

三、 页面与路由调整

1. 创建个人中心页面
  1. src/views/ 目录下创建 profile 文件夹。
  2. 在文件夹内创建 index.vue 组件文件,作为个人中心主页。
2. 添加个人中心路由

在路由配置文件(如 src/router/index.js 或对应的模块文件)中添加路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
path: '/profile',
component: Layout, // 使用主布局
redirect: '/profile/index',
hidden: true, // 不在侧边栏显示
children: [
{
path: 'index',
component: () => import('@/views/profile/index'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user', noCache: true }
}
]
}
3. 修改导航栏下拉菜单 (src/layout/components/Navbar.vue)

将用户下拉菜单中的链接指向首页和个人中心。

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<!- 简化示例 ->
<router-link to="/">
<el-dropdown-item>首页</el-dropdown-item>
</router-link>
<router-link to="/profile">
<el-dropdown-item>我的主页</el-dropdown-item>
</router-link>
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">退出</span>
</el-dropdown-item>
</template>

四、 测试与验证

  1. 启动服务:确保后端 API 服务已运行。
  2. 登录测试:在前端登录页面,使用后端有效的账号(邮箱)和密码登录。观察网络请求,确认 Token 被正确设置和携带。
  3. 页面跳转:登录后,点击右上角用户头像,检查下拉菜单中的“我的主页”是否能正确跳转到个人中心页面。
  4. 数据请求:测试其他数据接口(如 Table 列表),确认能正常获取并渲染后端返回的数据。

五、 关键注意事项

  1. **跨域问题 (CORS)**:开发时,确保后端已正确配置 CORS,允许前端域名/端口进行跨域请求。
  2. Token 存储与刷新:本文示例将 Token 存储在 Cookie 和 Vuex 中。生产环境需要考虑 Token 的自动刷新机制。
  3. 接口格式统一:确保前端 Axios 拦截器中关于响应成功 (code) 的判断与后端所有接口的返回格式严格一致。
  4. 路由权限:如果后端返回的用户角色/权限信息结构有变,需要同步修改 src/permission.js 和侧边栏生成逻辑。

六、 参考资源

原文链接:Git应用详解第十讲:Git子库:submodule与subtree

一个中大型项目往往会依赖几个模块,git提供了子库的概念。可以将这些子模块存放在不同的仓库中,通过submodulesubtree实现仓库的嵌套。本讲为Git应用详解的倒数第二讲,胜利离我们不远了!

一、submodule

submodule:子模块的意思,表示将一个版本库作为子库引入到另一个版本库中:

img

1.引入子库

需要使用如下命令:

git submodule add 子库地址 保存目录

比如:

1
git submodule add git@github.com:AhuntSun/git_child.git mymodule

执行上述命令会将地址对应的远程仓库作为子库,保存到当前版本库的mymodule目录下:

img

随后查看当前版本库的状态:

image-20200329203048016

可以发现新增了两个文件。查看其中的.gitmodules文件:

image-20200329203507411

可以看到当前文件的路径和子模块的url,随后将这两个新增文件添加提交推送。在当前仓库git_parent对应的远程仓库中多出了两个文件:

image-20200329203746236

其中mymodule文件夹上的3bd7f76 对应的是子仓库git_child中的最新提交

image-20200329203905051

点击mymodule文件夹,会自动跳转到子仓库中:

image-20200329203957392

通过上述分析,可以得出结论:两个仓库已经关联起来了,并且仓库git_child为仓库git_parent的子仓库;

2.同步子库变化

当被依赖的子版本库发生变化时:在子版本库git_child中新增文件world.txt并提交到远程仓库:

image-20200329204252524

这个时候依赖它的父版本库git_parent要如何感知这一变化呢?

方法一

这个时候git_parent只需要进入存放子库git_child的目录mymodule,执行git pull就能将子版本库git_child的更新拉取到本地:

image-20200330102106961

方法二

当父版本库git_parent依赖的多个子版本库都发生变化时,可以采用如下方法遍历更新所有子库:首先回到版本库主目录,执行以下指令:

1
git submodule foreach git pull

该命令会遍历当前版本库所依赖的所有子版本库,并将它们的更新拉取到父版本库git_parent

image-20200330102642607

拉取完成后,查看状态,发现mymodule目录下文件发生了变化,所以需要执行一次添加、提交、推送操作:

image-20200330102914556

3.复制父版本库

如果将使用了submodule添加依赖了子库的父版本库git_parent,克隆一份到本地的话。在克隆出来的新版本库git_parent2中,原父版本库存放依赖子库的目录虽在,但是内容不在:

image-20200330103417911

进入根据git_parent复制出来的仓库git_parent2,会发现mymodule目录为空:

image-20200330103502848

解决方法:可采用多条命令的分步操作,也可以通过参数将多步操作进行合并。

分步操作

这是在执行了clone操作后的额外操作,还需要做两件事:

  • 手动初始化submodule

    1
    git submodule init
  • 手动拉取依赖的子版本库;:

    1
    git submodule update --recursive

image-20200330103803762

执行完两步操作后,子版本库中就有内容了。由此完成了git_parent的克隆;

合并操作

分步操作相对繁琐,还可以通过添加参数的方式,将多步操作进行合并。通过以下指令基于git_parent克隆一份git_parent3

1
git clone git@github.com:AhuntSun/git_parent.git git_parent3 --recursive

image-20200330104210732

--recursive表示递归地克隆git_parent依赖的所有子版本库。

4.删除子版本库

git没有提供直接删除submodule子库的命令,但是我们可以通过其他指令的组合来达到这一目的,分为三步:

  • submodule从版本库中删除:

    1
    git rm --cache mymodule

image-20200330105131697

git rm的作用为删除版本库中的文件,并将这一操作纳入暂存区;

  • submodule从工作区中删除;

image-20200330105226923

  • 最后将.gitmodules目录删除;

image-20200330105542069

完成三步操作后,再进行添加,提交,推送即可完成删除子库的操作:

image-20200330105614793

二、subtree

1.简介

subtreesubmodule的作用是一样的,但是subtree出现得比submodule晚,它的出现是为了弥补submodule存在的问题:

  • 第一:submodule不能在父版本库中修改子版本库的代码,只能在子版本库中修改,是单向的;
  • 第二:submodule没有直接删除子版本库的功能;

subtree则可以实现双向数据修改。官方推荐使用subtree替代submodule

2.创建子库

首先创建两个版本库:git_subtree_parentgit_subtree_child然后在git_subtree_parent中执行git subtree会列出该指令的一些常见的参数:

image-20200330112616987

3.建立关联

首先需要给git_subtree_parent添加一个子库git_subtree_child:

第一步:添加子库的远程地址:

1
git remote add subtree-origin git@github.com:AhuntSun/git_subtree_child.git

添加完成后,父版本库中就有两个远程地址了:

image-20200330113223780

这里的subtree-origin就代表了远程仓库git_subtree_child的地址。

第二步:建立依赖关系:

1
2
git subtree add --prefix=subtree subtree-origin master --squash
//其中的--prefix=subtree可以写成:--p subtree 或 --prefix subtree

该命令表示将远程地址为subtree-origin的,子版本库上master分支的,文件克隆到subtree目录下;

注意:是在某一分支(如master)上将subtree-origin代表的远程仓库的某一分支(如master)作为子库拉取到subtree文件夹中。可切换到其他分支重复上述操作,也就是说子库的实质就是子分支。

--squash是可选参数,它的含义是合并,压缩的意思。

  • 如果不增加这个参数,则会把远程的子库中指定的分支(这里是master)中的提交一个一个地拉取到本地再去创建一个合并提交;
  • 如果增加了这个参数,会将远程子库指定分支上的多次提交合并压缩成一次提交再拉取到本地,这样拉取到本地的,远程子库中的,指定分支上的,历史提交记录就没有了。

image-20200330114203889

拉取完成后,父版本库中会增添一个subtree目录,里面是子库的文件,相当于把依赖的子库代码拉取到了本地:

image-20200330114316257

此时查看一下父版本库的提交历史:
image-20200330114500554

会发现其中没有子库李四的提交信息,这是因为--squash参数将他的提交压缩为一次提交,并由父版本库张三进行合并和提交。所以父版本库多出了两次提交。

随后,我们在父版本库中进行一次推送:

image-20200330114730534

结果远程仓库中多出了一个存放子版本库文件的subtree目录,并且完全脱离了版本库git_subtree_child,仅仅是属于父版本库git_subtree_parent的一个目录。而不像使用submodule那样,是一个点击就会自动跳转到依赖子库的指针

  • subtree的远程父版本库:

image-20200330115004586

  • submodule的远程父版本库:

image-20200329203746236

submodulesubtree子库的区别为:

image-20200408224805624

4.同步子库变化

在子库中创建一个新文件world并推送到远程子库:
image-20200330115440136

在父库中通过如下指令更新依赖的子库内容:

1
git subtree pull --prefix=subtree subtree-origin master --squash

image-20200330115726052

此时查看一下提交历史:

image-20200330115755340

发现没有子库李四的提交信息,这都是--squash的作用。子库的修改交由父库来提交。

5.参数--squash

该参数的作用为:防止子库指定分支上的提交历史污染父版本库。比如在子库的master分支上进行了三次提交分别为:abc,并推送到远程子库。

首先,复习一下合并分支时遵循的三方合并原则:

image-20200408003842196

当提交46需要合并的时候,git会先寻找二者的公共父提交节点,如图中的2,然后在提交2的基础上进行246的三方合并,合并后得到提交7

父仓库执行pull操作时:如果添加参数--squash,就会把远程子库master分支上的这三次提交合并为一次新的提交abc;随后再与父仓库中子库的master分支进行合并,又产生一次提交X。整个pull的过程一共产生了五次提交,如下图所示:

image-20200420103912282

存在的问题:

由于--squash指令的合并操作,会导致远程master分支上的合并提交abc与本地master分支上的最新提交2,找不到公共父节点,从而合并失败。同时push操作也会出现额外的问题。

最佳实践:要么全部操作都使用--squash指令,要么全部操作都不使用该参数,这样就不会出错。

错误示范:

为了验证,重新创建两个仓库AB,并通过subtreeB设置为A的子库。这次全程都没有使用参数--squash,重复上述操作:

  • 首先,修改子库文件;
  • 然后,通过下列指令,在不使用参数--squash的情况下,将远程子库A变化的文件拉取到本地:
1
git subtree pull --prefix=subtree subtree-origin master

image-20200330141920474

此时查看提交历史:

image-20200330142000915

可以看到子库儿子的提交信息污染了父版本库的提交信息,验证了上述的结论。

所以要么都使用该指令,要么都不使用才能避免错误;如果不需要子库的提交日志,推荐使用--squash指令。

补充:echo 'new line' >> test.txt:表示在test.txt文件末尾追加文本new line;如果是一个>表示替换掉test.txt内的全部内容。

6.修改子库

subtree的强大之处在于,它可以在父版本库中修改依赖的子版本库。以下为演示:

进入父版本库存放子库的subtree目录,修改子库文件child.txt,并推送到远程父仓库:

image-20200330121429186

此时远程父版本库中存放子库文件的subtree目录发生了变化,但是独立的远程子库git_subtree_child并没有发生变化。

  • 修改独立的远程子库:

    可执行以下命令,同步地修改远程子版本库:

    1
    git subtree push --prefix=subtree subtree-origin master

    如下图所示,父库中的子库文件child.txt新增的child2内容,同步到了独立的远程子库中:

    image-20200330125911158

  • 修改独立的本地子库:

    回到本地子库git_subtree_child,将对应的远程子库进行的修改拉取到本地进行合并同步:

    image-20200330144044823

    由此无论是远程的还是本地的子库都被修改了。

实际上使用subtree后,在外部看起来父仓库和子仓库是一个整体的仓库。执行clone操作时,不会像submodule那样需要遍历子库来单独克隆。而是可以将整个父仓库和它所依赖的子库当做一个整体进行克隆。

存在的问题

父版本库拉取远程子库进行更新同步会出现的问题:

  • 子仓库第一次修改:

    经历了上述操作,本地子库与远程子库的文件达到了同步,其中文件child.txt的内容都是child~4。在此基础上本地子库为该文件添加child5~6

    image-20200330145702019

    然后推送到远程子库。

  • 父仓库第一次拉取:

    随后父版本库通过下述指令,拉取远程子库,与本地父仓库git_subtree_parent中的子库进行同步:

    1
    git subtree pull --p subtree subtree-origin master --squash

    结果出现了合并失败的情况:

    image-20200330145839093

    我们查看冲突产生的文件:

    image-20200330145922152

    发现父版本库中的子库与远程子库内容上并无冲突,但是却发生了冲突,这是为什么呢?

    探究冲突产生的原因之前我们先解决冲突,先删除多余的内容:

    image-20200330150141430

    随后执行git add命令和git commit命令标识解决了冲突:

    image-20200330150312944

    image-20200330150406317

    解决完冲突后将该文件推送到独立的远程子库,发现文件并没有发生更新,也就是说git认为我们并没有解决冲突:

    image-20200330150747452

  • 子仓库第二次修改与父仓库第二次拉取:

    再次修改本地子库的文件并推送到对应的远程仓库,父版本库再次将远程子库更新的文件拉取到本地进行同步:

    image-20200330151140092

    这次却成功了!为什么同样的操作,有的时候成功有的时候失败呢?

解决方案

原因出现在--squash指令中。实际上,--squash指令把子库中的提交信息合并了,导致父仓库在执行git pull操作时找不到公共的父节点,从而导致即使文件没有冲突的内容,也会出现合并冲突的情况。其实不使用--squash也会有这种问题,问题的根本原因仍然是三方合并时找不到公共父节点。我们打开gitk

image-20200330154944300

从图中不难看出,当使用subtree时,子库与父库之间是没有公共节点的,所以时常会因为找不到公共节点而出现合并冲突的情况,此时只需要解决冲突,手动合并即可。

不使用subtree时,普通的版本库中的各分支总会有一个公共节点:

image-20200330160206258

再次强调:使用--squash指令时一定要小心,要么都使用它,要么都不使用。

7.抽离子库

git subtree split

当开发过程中出现某些子库完全可以复用到其他项目中时,我们希望将它独立出来。

  • 方法一:可以手动将文件拷贝出来。缺点是,这样会丢失关于该子库的提交记录;

  • 方法二:

    使用

    1
    git subtree split

    指令,该指令会把关于独立出来的子库的每次提交都记录起来。但是,这样存在弊端:

    • 比如该独立子库为company.util,当一次提交同时修改了company.utilcompany.server两个子库时。
    • 通过上述命令独立出来的子库util只会记录对自身修改的提交,而不会记录对company.server的修改,这样在别人看来这次提交就只修改了util,这是不完整的。

来源:https://blog.guoqianfan.com/2019/11/24/timestamp-in-csharp/

什么是时间戳

时间戳默认是Unix时间戳

首先要清楚JavaScript与Unix的时间戳的区别:

JavaScript时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总毫秒数

Unix时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数

可以看出JavaScript时间戳是总毫秒数,Unix时间戳是总秒数

比如同样是的 2016/11/03 12:30:00 ,转换为JavaScript时间戳为 1478147400000;转换为Unix时间戳为 1478147400。

从上面也可以看出时间戳与时区无关

Unix时间戳相互转换

C# DateTime转换为Unix时间戳

.NET 4.6新方法

只能在 .NET 4.6及更高版本里才能使用。

1
2
long timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds(); 
Console.WriteLine(timeStamp);

通用的老方法

1
2
3
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1)); 
long timeStamp = (long)(DateTime.Now - startTime).TotalSeconds;
System.Console.WriteLine(timeStamp);

Unix时间戳转换为C# DateTime

.NET 4.6新方法

由时间戳转换的DateTimeOffset的时区默认是+00:00,此时我们需要转为本地时区,否则后续使用可能会有问题。

转为本地时区:DateTimeOffset.LocalDateTime

示例代码如下:

1
2
3
4
5
6

DateTimeOffset dto = DateTimeOffset.FromUnixTimeMilliseconds(1573696406184);

DateTime dt01 = dto.DateTime;

DateTime dt02 = dto.LocalDateTime;

通用的老方法

1
2
3
4
long unixTimeStamp = 1478162177;
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1));
DateTime dt = startTime.AddSeconds(unixTimeStamp);
System.Console.WriteLine(dt.ToString("yyyy/MM/dd HH:mm:ss:ffff"));

备注

DateTimeOffset使用Now还是UtcNow

对于DateTimeOffset,发现有2个获取当前时间的属性:DateTimeOffset.NowDateTimeOffset.UtcNow

如果只是获取时间戳,这2个使用哪个都可以,得到的值是一样的。

因为DateTimeOffset里面有时区信息,获取时间戳时会使用时区进行转换的,所以获得的时间戳一样。

而也是因为时区的原因,DateTimeOffset的其他操作可能会不一样。例如DateTimeOffset.DateTime就不一样,此时推荐使用DateTimeOffset.LocalDateTime来获得本地时区的时间。

测试代码如下:

1
2
3
4
5
6
7
8
9

Console.WriteLine("none:{0}", DateTimeOffset.Now);

Console.WriteLine("utc:{0}", DateTimeOffset.UtcNow);


Console.WriteLine("none:{0}", DateTimeOffset.Now.ToUnixTimeSeconds());

Console.WriteLine("utc:{0}", DateTimeOffset.UtcNow.ToUnixTimeSeconds());

DateTime转换为DateTimeOffset

可以直接把DateTime赋值给DateTimeOffset,内部会自动进行隐式转换。这里涉及到时区,请往下看。

DateTime的时区信息(Kind属性)

DateTime时区信息存放在Kind属性里。Kind属性的数据类型是DateTimeKind枚举,只有3个值:

  • Unspecified:未指定/未规定
  • UtcUTC时间
  • Local:本地时区

不同情况下得到的DateTimeKind是不同的,具体如下:

  • DateTime.NowDateTime.Kind是 **Local(本地时区)**。

  • DateTime.UtcNowDateTime.Kind是 **Utc**。

  • DateTime.Parse()

    • 默认】在未指定时区时,DateTime.KindUnspecified

    • 指定时区:指定时区后DateTime.Kind就是相对应的值。

      指定时区有2种方式:

      • 默认+优先待转换的字符串里有时区信息。例如:2019/11/24 17:40:32 +08:00
      • 使用DateTimeStyles参数来指定时区。DateTimeStyles是枚举类型,更多信息自己查看定义,这里不再多说。

LocalUtc都会把相应的时区传递过去。对于 Unspecified(未指定),会被当做本地时区来处理(结果已验证,源码没看懂)。

测试代码

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

DateTime dtNow = DateTime.Now;

DateTime dtUtcNow = DateTime.UtcNow;

DateTime dtParse = DateTime.Parse("2019-11-24 17:40:13");


DateTimeOffset dtoNow = dtNow;

DateTimeOffset dtoUtcNow = dtUtcNow;

DateTimeOffset dtoParse = dtParse;

Console.WriteLine("DateTime:");
Console.WriteLine("dtNow:{0}(Kind:{1})", dtNow, dtNow.Kind);
Console.WriteLine("dtUtcNow:{0}(Kind:{1})", dtUtcNow, dtUtcNow.Kind);
Console.WriteLine("dtParse:{0}(Kind:{1})", dtParse, dtParse.Kind);

Console.WriteLine();

Console.WriteLine("DateTimeOffset:");
Console.WriteLine("dtoNow:{0}", dtoNow);
Console.WriteLine("dtoUtcNow:{0}", dtoUtcNow);
Console.WriteLine("dtoParse:{0}", dtoParse);

输出结果如下:

1
2
3
4
5
6
7
8
9
DateTime:
dtNow:2019/11/24 17:40:32(Kind:Local)
dtUtcNow:2019/11/24 9:40:32(Kind:Utc)
dtParse:2019/11/24 17:40:13(Kind:Unspecified)

DateTimeOffset:
dtoNow:2019/11/24 17:40:32 +08:00
dtoUtcNow:2019/11/24 9:40:32 +00:00
dtoParse:2019/11/24 17:40:13 +08:00

DateTimeOffset.Parse的默认时区

DateTimeOffset.Parse的默认时区是当前时区

1
2

Console.WriteLine("parse:{0}", DateTimeOffset.Parse("2019-6-14 15:38:49"));

参考

  1. C# DateTime与时间戳转换:https://www.cnblogs.com/polk6/p/6024892.html
  2. 如何将Unix时间戳转换为DateTime,反之亦然?:https://stackoverflow.com/questions/249760/how-can-i-convert-a-unix-timestamp-to-datetime-and-vice-versa
  3. DateTimeOffset源码:https://source.dot.net/#System.Private.CoreLib/DateTimeOffset.cs

一、核心概念:什么是内核模块?

内核模块(Kernel Module)是 Linux 内核的一种可动态加载/卸载的扩展组件。它们允许在不重新编译整个内核的情况下,为系统添加新的硬件支持、文件系统或内核功能。模块通常以 .ko(Kernel Object)为扩展名。

主要优势

  • 灵活性:按需加载,减少内核内存占用。
  • 可维护性:更新驱动或功能时无需重启系统。
  • 模块化:保持内核核心精简,通过模块扩展功能。

二、模块管理工具对比

Linux 提供了两套工具来管理内核模块,各有其适用场景。

工具 命令 特点 适用场景
智能管理工具 modprobe 1. 自动处理模块依赖关系
2. 使用模块名(无需路径)
3. 从标准模块目录搜索
日常使用首选,特别是加载复杂驱动
基础加载工具 insmod 1. 需指定模块完整路径
2. 不处理依赖关系
3. 直接系统调用
低级操作,调试或已知无依赖的简单模块
基础卸载工具 rmmod 1. 使用模块名卸载
2. 不处理依赖关系
卸载由 insmod 加载的模块
查看工具 lsmod 显示当前已加载的所有模块 状态检查

三、使用 modprobe(推荐方法)

modprobe 是智能化的模块管理工具,它会读取 /lib/modules/$(uname -r)/modules.dep 文件中的依赖关系,自动加载所需的所有模块。

3.1 准备工作:生成模块依赖关系

在首次使用或安装新模块后,建议先更新模块依赖数据库:

1
sudo depmod -a

此命令会扫描 /lib/modules/$(uname -r)/ 目录下的所有模块,生成 modules.dep 文件,其中记录了模块间的依赖关系。

3.2 加载模块

1
2
3
4
5
# 加载指定模块(自动处理依赖)
sudo modprobe vfat

# 加载所有模块(不常用)
sudo modprobe -a

注意:只需提供模块名(如 vfat),无需 .ko 后缀和路径。

3.3 卸载模块

1
2
# 卸载指定模块(自动尝试卸载依赖模块)
sudo modprobe -r vfat

3.4 常用 modprobe 参数详解

参数 功能
-a, --all 加载所有模块。
-r, --remove 卸载指定模块。
-l, --list 列出所有可用模块(带完整路径)。
-c, --show-conf 显示所有模块的配置信息(如别名)。
-d, --debug 启用调试模式。
-v, --verbose 显示详细执行信息。
--help 显示帮助信息。

3.5 实用示例

1
2
3
4
5
6
7
8
9
10
# 1. 查看模块配置信息(如别名)
sudo modprobe -c | grep vfat
# 输出示例:alias fs-vfat vfat

# 2. 列出所有可用模块
sudo modprobe -l
# 输出示例:/lib/modules/5.4.0-xx/kernel/fs/vfat/vfat.ko

# 3. 带详细信息加载模块
sudo modprobe -v vfat

四、使用 insmodrmmod(基础方法)

这对工具提供了更底层的控制,但需要手动处理依赖关系。

4.1 加载模块 (insmod)

1
2
# 必须指定模块的完整路径
sudo insmod /lib/modules/$(uname -r)/kernel/drivers/block/floppy.ko

关键限制:如果 floppy.ko 依赖其他模块(如特定总线驱动),insmod 会直接失败并报错,而不会自动加载依赖项。

4.2 卸载模块 (rmmod)

1
2
# 使用模块名(不含.ko后缀和路径)
sudo rmmod floppy

重要:只能卸载当前未被使用且没有其他模块依赖的模块。

4.3 insmod 常用参数

参数 功能
-f 强制加载,忽略内核版本检查(危险)。
-k 将模块标记为“自动卸载”。
-o <名称> 指定加载后模块的名称(可不同于文件名)。
-p 仅测试模块是否能加载,不实际加载。
-s 将操作信息写入系统日志。
-v 显示详细信息。

示例:强制加载并输出详细信息

1
sudo insmod -f -v /path/to/module.ko

五、模块状态查看与系统集成

5.1 查看已加载模块

1
2
3
4
5
# 查看所有已加载模块
lsmod

# 配合grep过滤
lsmod | grep vfat

输出格式

1
2
3
Module                  Size  Used by
vfat 24576 0
fat 73728 1 vfat
  • Module:模块名称。
  • Size:模块占用的内存大小(字节)。
  • Used by:被哪些模块或进程使用(数字表示引用计数)。

5.2 模块配置文件

模块的别名、黑名单等配置位于:

  • /etc/modprobe.d/ 目录下的 .conf 文件
  • /etc/modules-load.d/ 目录(定义启动时自动加载的模块)

示例:创建文件 /etc/modprobe.d/blacklist.conf 可禁用特定模块

1
2
# 禁用有问题的无线网卡驱动
blacklist acer_wmi

5.3 开机自动加载模块

方法一:编辑 /etc/modules 文件(传统方法)

1
2
3
4
sudo nano /etc/modules
# 添加模块名,每行一个
vfat
nvidia

方法二:使用 modules-load.d 目录(现代方法)

1
2
3
# 创建配置文件
echo "vfat" | sudo tee /etc/modules-load.d/vfat.conf
echo "nvidia" | sudo tee /etc/modules-load.d/nvidia.conf

六、故障排除与最佳实践

6.1 常见错误与解决

  • 错误:insmod: ERROR: could not insert module: Invalid module format
    原因:模块编译时的内核版本与当前运行内核不匹配。
    解决:重新编译模块或寻找对应版本的模块。

  • 错误:modprobe: FATAL: Module xxx not found
    原因:模块未安装或不在标准搜索路径。
    解决

    1. 检查模块是否存在:find /lib/modules -name "*.ko" | grep xxx
    2. 运行 sudo depmod -a 更新数据库。
  • 错误:rmmod: ERROR: Module xxx is in use
    原因:模块正在被进程或其他模块使用。
    解决:先停止相关进程,或使用 modprobe -r 尝试智能卸载。

6.2 依赖关系解析示例

假设模块 A.ko 依赖于 B.ko

1
2
3
4
5
6
7
8
9
# 使用 insmod 会失败
sudo insmod /path/to/A.ko # 失败:未加载 B.ko

# 需要先加载依赖
sudo insmod /path/to/B.ko
sudo insmod /path/to/A.ko # 成功

# 使用 modprobe 自动处理
sudo modprobe A # 自动先加载 B.ko,再加载 A.ko

6.3 最佳实践总结

  1. **日常操作首选 modprobe**:让系统自动处理复杂的依赖关系。
  2. **调试时使用 insmod/rmmod**:当需要精确控制加载顺序或排查依赖问题时。
  3. 始终更新依赖数据库:安装新内核或模块后,运行 sudo depmod -a
  4. 检查模块使用情况:卸载前使用 lsmod | grep <模块名> 查看引用计数。
  5. 利用配置目录:将自定义设置放在 /etc/modprobe.d/ 中,便于管理。

七、总结对比

操作 modprobe 方式 insmod/rmmod 方式
加载模块 sudo modprobe <模块名> sudo insmod <完整路径>
卸载模块 sudo modprobe -r <模块名> sudo rmmod <模块名>
依赖处理 自动,读取 modules.dep 手动,需按顺序加载
模块查找 搜索标准目录(/lib/modules/ 需提供完整路径
适用场景 生产环境、日常管理 开发调试、低级操作

最终建议:对于大多数用户和管理员,modprobe 是更安全、便捷的选择。只有在深入了解模块依赖关系,或进行系统级调试时,才需要使用 insmodrmmod 这对底层工具。

0%