Vim 中已经提供了非常多移动的动作,从简单的字符间移动 (jkhl),到 word 间 (w/e/b),句子间 ((
/)
),段落间 ({/}),行首行尾 (0/^/$),文档开始 (gg),文档结尾 (G),还有搜索 (/
/np
) 等等一系列的操作,但 easy-motion 将 Vim 中的移动又提升了一个高度。
继续往下阅读之前先确保阅读了 vim 文档中关于 motion 的内容。
想象一个场景,想要跳转到当前行下一个段落中的第二个句子的第三个单词开头,使用上面提到的方法,可能需要按下不同的按键,并且可能还需要组合使用,那有没有什么方法能降低这个移动(或者说跳转)操作的复杂度呢?答案就是 easy-motion。不知道有多少人用过 Chrome 下的 Vimium 插件,在网页中按下 f
,页面中每一个能点击的地方都会显示几个字符,然后按下字符就会相当于在页面上点击,easy-motion 也使用相同的方式实现这一功能。
easy-motion 插件提供了更强的移动操作,在 easymotion 的官方文档中是这样定义 easymotion 的,easymotion 可以将 <number>w
或者 <number>f{char}
简化为几个按键的操作,举一个简单的例子,在 easymotion 下,如果按下 w
,那么 easymotion 会高亮所有 w
(下一个词首) 的结果,然后只需要按下一个键,就可以跳转到任何 w
按键按下后的目标地址。
Plug 'easymotion/vim-easymotion'
:help easymotion
Easymotion 的触发需要按下两次 <leader><leader>
,当然推荐熟悉之后使用 vim 的 map 配置更改一下 leader.
Default Mapping | Details
---------------------|----------------------------------------------
<Leader>f{char} | Find {char} to the right. See |f|.
<Leader>F{char} | Find {char} to the left. See |F|.
<Leader>t{char} | Till before the {char} to the right. See |t|.
<Leader>T{char} | Till after the {char} to the left. See |T|.
<Leader>w | Beginning of word forward. See |w|.
<Leader>W | Beginning of WORD forward. See |W|.
<Leader>b | Beginning of word backward. See |b|.
<Leader>B | Beginning of WORD backward. See |B|.
<Leader>e | End of word forward. See |e|.
<Leader>E | End of WORD forward. See |E|.
<Leader>ge | End of word backward. See |ge|.
<Leader>gE | End of WORD backward. See |gE|.
<Leader>j | Line downward. See |j|.
<Leader>k | Line upward. See |k|.
<Leader>n | Jump to latest "/" or "?" forward. See |n|.
<Leader>N | Jump to latest "/" or "?" backward. See |N|.
<Leader>s | Find(Search) {char} forward and backward.
| See |f| and |F|.
下面将 <leader><leader>
简写成 <ll>
向后向前跳转
<ll>w
<ll>b
双向跳转
<ll>s
向上向下跳转到行:
<ll>j
<ll>k
虽然使用了很长时间的 Vim,也使用了很长时间的 IntelliJ IDEA,但总感觉没有充分利用,所以想再这里总结一下,系统的浏览一遍 Idea Vim 插件能提供的功能,看看能不能有所受益,Vim 和 IntelliJ IDEA 的基本操作和内容就省略了。
首先 ideavim 这个插件是 JetBrains 官方提供的,基本上安装后即可。GitHub 的页面还提到 ideavim 插件提供了一些 Vim 插件的扩展功能,比如:
可以根据这个页面 上的方式配置和开启这个扩展功能。
~/.ideavimrc
来复用 Vim 的工作方式,以及充分利用 Idea 提供的 ActionIdea 中的 vim-easymotion 插件支持的配置,可以参考这里
在了解 easymotion 时意外收获了 AceJump 插件,IntelliJ IDEA 中的 easymotion 实际上是通过 AceJump 插件来实现的。 :q
默认情况下,使用 Ctrl + ; 来开启 AceJump 模式,不过我的 Ctrl + ;
已经作为输入法的多粘贴板来使用了,所以就改成 Alt + k 。
AceJump 的工作流程,按下 Alt + K 进入 AceJump ,此时按下任何按键就会在当前文件搜索,并给每一个结果一个 tag,按下回车,然后输入 tag 就可以快速跳转过去。
在 IntelliJ IDEA 中,任何选项操作都会映射到一个 action
上,点击按钮,就执行对应的 action
,所以记住 Ctrl + Shift + a 这个快捷键。
在编辑器模式下,可以输入如下命令查看 actionlist:
:actionlist
启用方式:
set surround
支持的 Commands: ys, cs, ds, S
下面的例子中,假设 *
是当前光标的位置:
Old Text | Command | Text After command execute |
---|---|---|
“Hello *world!” | ds” | Hello world! |
[123 + 4*56]/2 | cs]) | (123+456)/2 |
“Look ma, I’m *HTML!” | cs” | <q>Look ma, I'm HTML!</p> |
if *x>3 | ysW( | if ( x>3 ) |
my $str = *www; |
vllS’ | my $str = 'www'; |
vim-surround 在想要改变 surround
的时候非常方便。
目前我的使用场景大部分通过 IDEA 自带的 rename 功能批量替换变量即可做到,所以目前还没有开启这个功能的需求,更多多光标的操作技巧可以参考这篇文章
IdeaVim 支持的所有快捷键:
最后,这里 是我的 .ideavimrc
配置。
前些天正好在我的二代树莓派上安装了 AdGuard Home,这样一个基础服务必然不能少了监控,所以正好把 Prometheus node-exporter 安装一下。
首先确认一下 CPU 型号,我的是二代,比较老,直接 lscpu
看一下就知道:
Model name: ARMv7 Processor rev 5 (v7l)
这是一个 armv5 版本的,然后到 node-exporter 下载二进制:
注意选对版本。
wget https://github.com/prometheus/node_exporter/releases/download/v1.0.1/node_exporter-1.0.1.linux-armv5.tar.gz
然后接着这篇文章 即可。
最后验证:
curl http://localhost:9100/metrics
添加到 Prometheus Server
- job_name: 'raspberry pi node exporter'
scrape_interval: 5s
scrape_timeout: 5s
static_configs:
- targets: ['192.168.2.3:9100']
之后 Grafana 添加数据源,和之前的文章 一样。
之前也总结过两篇文章,我是如何使用 Clonezilla 进行全盘备份和恢复的 以及备份 Linux 过程中遇到的问题,今天这篇就记录一下恢复之前备份过的 Windows
我先来还原一下现在情况,原来我有两台小米的 Air 笔记本,所有的配置一样,不过一台我从之前的电脑上恢复了一个 Linux Mint 的系统,暂且叫这台 A1 笔记本,然后还有一台是默认的 Windows 系统,不过这一台用的比较少,暂且叫这台 A2 笔记本。前段时间我把 A2 笔记本使用 Clonezilla 备份了一下生成了一个从 device 到 image 的镜像,然后我把 A2 笔记本卖了,所以现在只剩下 A1 笔记本。
我在卖 A2 笔记本的时候,当时也做了系统的恢复,就是把当年 A1 原始的默认 Windows 系统恢复到了 A2 笔记本上,正好省去了我格式化硬盘,备份数据的苦恼,恢复上去之后 A2 没啥问题也可以直接启动。
但今天恢复 A1 笔记时,却遇到了一些问题,这里记录一下。
安装我之前的操作,恢复之前备份的 A2 的硬盘镜像文件到 A1 的整块硬盘上,官网的教程非常详细,这里就略过了,就安装默认的下一步下一步直接走了,可以所有操作完成后等等重启时,屏幕只出现了 “no bootable devices found”,我一想不应该呀,虽然用的是另一台机器的镜像恢复的数据,但是理论上应该还是能找到系统的。
之后还想着是不是引导坏了,还用着 Win PE 进去想修复一下,谁知道在 PE 里面根本找不到系统的硬盘,后来想想是不是恢复的时候把 MBR 搞坏了,还是说默认的 Windows 是安装在 GPT 分区表的硬盘上的。
然后使用如下的方法重新恢复了一次:
然后去 BIOS 中把之前引导 Linux 是的 Legacy 改成了 UEFI Mode,果然就能进入系统了。
Albert 是一个 Linux 上的启动器,使用 C++ 和 QT 实现,实现了如下的功能:
通过下面的网站下载官方编译的版本。
从源码编译安装:
git clone --recursive https://github.com/albertlauncher/albert.git
mkdir albert-build
cd albert-build
cmake ../albert -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
make
sudo make install
顺利的话可以走到最后一步,不过大概率会出现各种依赖的问题。
➜ cmake ../albert -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
CMake Error at /opt/qt59/lib/cmake/Qt5/Qt5Config.cmake:28 (find_package):
Could not find a package configuration file provided by "Qt5X11Extras" with
any of the following names:
Qt5X11ExtrasConfig.cmake
qt5x11extras-config.cmake
Add the installation prefix of "Qt5X11Extras" to CMAKE_PREFIX_PATH or set
"Qt5X11Extras_DIR" to a directory containing one of the above files. If
"Qt5X11Extras" provides a separate development package or SDK, be sure it
has been installed.
Call Stack (most recent call first):
lib/globalshortcut/CMakeLists.txt:16 (find_package)
-- Configuring incomplete, errors occurred!
安装缺失的依赖:
➜ sudo apt install libqt5x11extras5 libqt5x11extras5-dev
➜ cmake ../albert -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
CMake Error at /usr/lib/x86_64-linux-gnu/cmake/Qt5/Qt5Config.cmake:28 (find_package):
Could not find a package configuration file provided by "Qt5Svg" with any
of the following names:
Qt5SvgConfig.cmake
qt5svg-config.cmake
Add the installation prefix of "Qt5Svg" to CMAKE_PREFIX_PATH or set
"Qt5Svg_DIR" to a directory containing one of the above files. If "Qt5Svg"
provides a separate development package or SDK, be sure it has been
installed.
Call Stack (most recent call first):
plugins/widgetboxmodel/CMakeLists.txt:7 (find_package)
-- Configuring incomplete, errors occurred!
安装缺失的依赖:
➜ sudo apt install libqt5svg5 libqt5svg5-dev
最近 FastJson 安全问题频发,所以 JSON 解析又被拉到台面上,而正好不久前看 Reddit 看到 Gson 的作者在推荐一个叫 Moshi 的库,这就花点时间看一下。1
序列化 Date 的时候不包含时区的信息
Date epoch = new Date(0);
String epochJson = new Gson().toJson(epoch);
// "Dec 31, 1969 7:00:00 PM"
RFC 3339 标准 里面规定的日期表示法:
2020-06-12T07:20:50.52Z
其中 T
用来分割前面的日期和后面的时间,而最后的 Z
表示这个时间是 UTC+0
,其他人看到这个时间就可以根据自己的时区进行转换。2
Gson 在序列化 HTML 标签时,会进行 HTML escaping 成:
private String e = "12 & 5";
private String f = "12 > 6"
"e":"12 \u0026 5"
"f":"12 \u003e 6"
@Json
来自定义 Field 名@HexColor int
的限定符可以让多个 JSON 映射到同一个 Java Typejava.*
, javax.*
, android.*
等等,以防止被 Lock 在某一个特殊 JDK 或者 Android 版本JsonElement
模型更加具体的使用方法可以参考源代码中的实现。
最基本的使用,序列化一个 Java Object 到 JSON,或者将 JSON 映射到 Java Object 里面。
将 JSON 字符串解析成 Java 对象
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<BlackjackHand> jsonAdapter = moshi.adapter(BlackjackHand.class);
BlackjackHand blackjackHand = jsonAdapter.fromJson(json);
System.out.println(blackjackHand);
如果是 JsonArray:
Moshi moshi = new Moshi.Builder().build();
Type listOfCardsType = Types.newParameterizedType(List.class, Card.class);
JsonAdapter<List<Card>> jsonAdapter = moshi.adapter(listOfCardsType);
List<Card> cards = jsonAdapter.fromJson(json);
序列化 Java Object:
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<BlackjackHand> jsonAdapter = moshi.adapter(BlackjackHand.class);
String json = jsonAdapter.toJson(blackjackHand);
System.out.println(json);
比如有一个 JSON 字符串:
String json = ""
+ "{"
+ " \"username\": \"jesse\","
+ " \"lucky number\": 32"
+ "}\n";
其中的 lucky number
是带空格的,假如要解析到 Java Object
public final class Player {
public final String username;
public final @Json(name = "lucky number") int luckyNumber;
}
可以看到使用 @Json
注解即可。
Moshi 只能使用 Builder 模式创建,看其源码会发现,构造函数并不是 public 的。唯一一个带参数的
Moshi(Builder builder) {
List<JsonAdapter.Factory> factories = new ArrayList<>(
builder.factories.size() + BUILT_IN_FACTORIES.size());
factories.addAll(builder.factories);
factories.addAll(BUILT_IN_FACTORIES);
this.factories = Collections.unmodifiableList(factories);
}
可以看到 Moshi 有一系列 adapter() 公开方法,通过 adapter() 方法可以返回一个 JsonAdapter<>
对象,之后的操作都在该 adapter
之上进行。
成员变量
private final List<JsonAdapter.Factory> factories;
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
说明:
factories
: 是 JsonAdapter.Factory 数组,工厂模式产生 JsonAdapterLookupChain
adapterCache
是一个 LinkedHashMap
用来保存 Object 到 JsonAdapter 的映射关系Moshi 类中初始化的时候有 5 个内置的 Adapter Factory.
static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
static {
BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
}
Moshi.Builder 是 Moshi 内部的一个类,用来创建 Moshi,它有一系列方法:
com.squareup.moshi.Moshi.Builder#add(java.lang.reflect.Type, com.squareup.moshi.JsonAdapter<T>)
com.squareup.moshi.Moshi.Builder#add(java.lang.reflect.Type, java.lang.Class<? extends java.lang.annotation.Annotation>, com.squareup.moshi.JsonAdapter<T>)
com.squareup.moshi.Moshi.Builder#add(com.squareup.moshi.JsonAdapter.Factory)
com.squareup.moshi.Moshi.Builder#add(java.lang.Object)
com.squareup.moshi.Moshi.Builder#addAll
com.squareup.moshi.Moshi.Builder#build
可以看到除了 overload 重载的 add()
方法外,就是 build
方法,而 add()
方法可以添加一系列类型。
JsonAdapter 的公开方法:
com.squareup.moshi.JsonAdapter#fromJson(com.squareup.moshi.JsonReader)
com.squareup.moshi.JsonAdapter#fromJson(okio.BufferedSource)
com.squareup.moshi.JsonAdapter#fromJson(java.lang.String)
com.squareup.moshi.JsonAdapter#toJson(com.squareup.moshi.JsonWriter, T)
com.squareup.moshi.JsonAdapter#toJson(okio.BufferedSink, T)
com.squareup.moshi.JsonAdapter#toJson(T)
com.squareup.moshi.JsonAdapter#toJsonValue
com.squareup.moshi.JsonAdapter#fromJsonValue
com.squareup.moshi.JsonAdapter#serializeNulls
com.squareup.moshi.JsonAdapter#nullSafe
com.squareup.moshi.JsonAdapter#nonNull
com.squareup.moshi.JsonAdapter#lenient
com.squareup.moshi.JsonAdapter#failOnUnknown
com.squareup.moshi.JsonAdapter#indent
JsonAdapter 有两个抽象方法需要实现 fromJson
和 toJson
。
大致就能看出 JsonAdapter 的主要功能:
fromJson
to toJson
的转换另一方面提供转换过程中的一些选项
serializeNulls
serializes nulls when encoding JSONnullSafe
support for reading and writingnonNull
调用时会返回一个不允许 null 值的 JsonAdapterlenient
宽容处理 JSONfailOnUnknown
在遇到未知的 name 或 value 时抛出 JsonDataException
异常indent
输出的 JSON 是格式化好的JsonAdapter 主要去处理各个类型的转换,需要实现如下三个方法:
static final JsonAdapter<Boolean> BOOLEAN_JSON_ADAPTER = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
writer.value(value.booleanValue());
}
@Override public String toString() {
return "JsonAdapter(Boolean)";
}
};
这个接口定义在抽象类 JsonAdapter 中,Factory 只需要实现一个方法:
public interface Factory {
@CheckReturnValue
@Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
}
Factory 看名字也能猜到这是一个工厂方法,用来创造给定 Type 给定 Annotation 类型的 Adapter,如果无法创建则返回 null.
在 Factory 的实现中,可能会使用 moshi.adapter 来构建 Adapter.
再看 Moshi.Builder 类成员
final List<JsonAdapter.Factory> factories = new ArrayList<>();
在源代码的基础上加了一些注释
Kie have these concepts which every user need to know.
通过如下方式产生 KieServices:
KieServices ks = KieServices.Factory.get();
KieService 可以用来创建 KieContainer。
KieContainer 定义了规则的范围。
KieContainer 是所有给定 KieModule 的 KieBases 的集合。
KieContainer 承载了 KieModule 和其依赖,一个层级的 KieModules 结构可以被加载到一个 KieContainer 实例中。
KieContainer 可以拥有一个或者多个 KieBases.
KieContainer 可以通过 KieService 产生:
KieContainer kContainer = ks.newKieClasspathContainer();
每一个 KieModule 包含了 business assets(包括了流程,规则,决策表等等)。
一个 KieModule 是一个标准的 Java-Maven 项目。
KieModule 在 org.kie.api.builder
包下,KieModule 是一个存放所有定义好的 KieBases 资源的容器,和 pom.xml
文件相似,定义了其 ReleaseId, kmodule.xml
文件定义了 KieBase
的名字,配置,以及其他用来创建 KieSession 的资源,以及其他用来 build KIEBases 的资源。
指定的文件 kmodule.xml
定义在 META-TNF/
目录下,一定了内部的资源如何分组如何配置等等信息。
KieModule 用来定义多个 KieBases 和 KieSessions。KieModule 可以包含其他 KieModules.
KieBase 是应用所有定义好的 Knowledge 合集,包括了 rules(规则), processes(流程), functions(方法), type models, KieBase 自身不包含任何运行时数据,sessions 可以从 KieBase 中创建,然后运行时数据可以被装入,并且通过 sessions 可以启动一个流程。
KieBase 代表着编译好的资源的版本,可以有 Stateless 和 Stateful Session,一个典型的使用场景是为一个 packages 使用一个 KieBase ,另一个 package 使用另一个 KieBase.
KieSession 是和工作流引擎交互的最常用的方式,KieSession 允许应用建立和引擎的交互,session 的状态会在每一次调用的时候保存下来。而流程会在一组相同的变量上触发多次。当应用完成调用,必须调用 dispose()
来释放资源以及使用的内存。
KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();
KieSession kSession = kContainer.newKieSession();
for( Object fact : facts ) {
kSession.insert( fact );
}
kSession.fireAllRules();
kSession.dispose();
每一个 KieBase 都可以有一个或者多个 KieSessions.
KieContainer 是线程安全的。
KieContainer.newStatelessKieSession()
和
KieContainer.newKieSession()
方法是线程不安全的。
Drools 的 Session 分为有状态和无状态。
stateful KieSession 可以在多次和 Rule Engine 交互的过程中保持状态。 而无状态的 KieSession 只允许我们交互一次,并直接获取结果。
Stateful 可以通过 insert
方法插入 Fact,并取得 FactHandle,通过这个 Handle 可以多次更新 Fact 从而触发规则
FactHandle handle = statefulKieSession.insert(factObject);
factObject.setBalance(100.0);
statefulKieSession.update(handle,factObject);
Stateless 类似一次函数调用,通过 execute
方法传入 Fact
,匹配规则
session.execute(Arrays.asList(new Object[]{routeResult,featureManager.getFreeFeatures(),accessManager,this}));
// 又或者,执行完获得结果:
List<Command> cmds = new ArrayList<>();
cmds.add(CommandFactory.newInsert(routeResult,"routeResult")); cmds.add(CommandFactory.newInsert(featureManager.getFreeFeatures(),"freeFeature"));
cmds.add(CommandFactory.newInsert(accessManager,"accessManager"));
cmds.add(CommandFactory.newInsert(this,"router"));
ExecutionResults results = statelessKieSession.execute( CommandFactory.newBatchExecution( cmds ) );
KieBuilder is a builder for the resources contained in a KieModule
KieServices ks = KieServices.Factory.get();
KieRepository kr = ks.getRepository();
InputStream is = new ByteArrayInputStream(bytes);
KieModule kModule = kr.addKieModule(ks.getResources().newInputStreamResource(is));
KieContainer kContainer = ks.newKieContainer(kModule.getReleaseId());
KieResources 可以从很多来源构造,字节流 (InputStream),文件系统 (File),ClassPath 等等。
KieModuleModel
KieRepository
KieContainerImpl
KieBase
KieSession
真正用来启动 Process 的类
ksession.startProcess()
源代码地址:
三个可用的镜像:
JBoss Business-Central Workbench 基础镜像,可以根据该镜像来扩展创建自己的镜像。
继承自 JBoss Business-Central Workbench 镜像,可以直接使用的 Docker 镜像。提供了自定义的配置文件,默认的用户和角色。
提供了可以立即执行的全部 jBPM 功能的镜像,包含全部必须的配置文件。包括 jBPM Workbench, Kie Server and jBPM Case Management Showcase。可以访问 新手教程 来查看。
Functional interface is a new feature Java 8 introduced. Functional interfaces provides target types for lambda expressions and method references.
// Assignment context
Predicate<String> p = String::isEmpty;
// Method invocation context
stream.filter(e -> e.getSize() > 10)...
// Cast context
stream.map((ToIntFunction) e -> e.getSize())...
相关的新的函数式接口定义在 java.util.function
包下 1:
void accept(T t)
R apply(T t)
T get()
boolean test(T t)
class Test
{
public static void main(String args[])
{
// create anonymous inner class object
new Thread(new Runnable()
{
@Override
public void run()
{
System.out.println("New thread created");
}
}).start();
}
}
使用函数式接口后
class Test
{
public static void main(String args[])
{
// lambda expression to create the object
new Thread(()->
{System.out.println("New thread created");}).start();
}
}
定义和使用
@FunctionalInterface
interface Square
{
int calculate(int x);
}
class Test
{
public static void main(String args[])
{
int a = 5;
// lambda expression to define the calculate method
Square s = (int x)->x*x;
// parameter passed and return type must be
// same as defined in the prototype
int ans = s.calculate(a);
System.out.println(ans);
}
}
首先来介绍一下这两个软件,SnapRAID 和 MergerFS,不同于其他现有的 NAS 系统,可以把 OpenMediaVault 看成一个简单的带有 Web UI 的 Linux 系统,他使用最基本的文件系统,没有 ZFS 的实时文件冗余,所以需要 SnapRAID 提供的冗余来保护硬盘数据安全。SnapRAID 需要一块单独的硬盘来存放校验数据,这个盘的容量必须大于等于其他任何一个数据盘。SnapRAID 采用快照的方式来做数据冗余,这种设计避免了所有硬盘在没有数据操作情况下也要运转来实时数据备份的消耗。
MergerFS 则是一个联合文件系统,可以将多块硬盘挂载到一个挂载点,通过 MergerFS 来自动决定数据该存储在哪块硬盘上。
先决条件:
openmediavault-snapraid
和 openmediavault-unionfilesystem
插件MergerFS 是一个联合文件系统 (union file system),MergerFS 会将多块硬盘,或者多个文件夹合并到 MergerFS pool 中,这样一个系统就会有一个统一的文件入口,方便管理。
选用 MergerFS 另外一个理由就是,通过 MergerFS 合并的目录并不会对数据进行条带化的处理,每块硬盘上还是保存原来的文件目录和文件,任意一块硬盘单独拿出来放到其他系统上,不需要额外的逻辑卷的配置,就可以直接挂载读取这个硬盘的数据。
通过如下步骤创建 MergerFS 磁盘池:
这样以后在文件系统中就会看到新创建的联合目录。在创建共享文件夹的时候就可以在合并的联合文件系统上进行。
在创建了 MergerFS Pool 后,在 OpenMediaVault 的文件目录 /srv
目录下会多出一个文件夹,这个文件夹就会存放 MergerFS Pool 中的数据。
SnapRAID 是一个磁盘阵列的冗余备份工具,它可以存储额外的奇偶校验信息用来校验数据,以便在磁盘发生故障时恢复数据。
SnapRAID 适用于家庭媒体服务器,适合于存储多数不经常变动的大文件场景。
特性:
SnapRAID 作为一个备份工具非常强大,强烈推荐阅读官网上关于 SnapRAID 和 [[unRAID]], ZFS 等系统或文件系统提供的备份的对比 1
在 OpenMediaVault 中使用 SnapRAID :
假设三块硬盘中前两块用来存储重要的数据,第三块用来存放奇偶校验信息。那么首先设置数据盘:
重复上面的步骤将第二块磁盘也添加进来。
添加奇偶校验信息盘的时候,和上面步骤相似。不过要注意的是在添加磁盘时
然后点击保存。
在添加完硬盘之后,可以进行同步操作:
最后设置 SnapRAID Scrub,scrubbing 的目的是检查数据盘和校验信息盘的错误。可以在 SnapRAID 的 settings 界面中启用 Scheduled diff,启用后会自动创建一个周期性 Crontab 任务。
对于一些不需要 SnapRAID 进行冗余校验的目录,可以在 Rules 选项中进行排除。比如说经常变动的 metadata 信息,Docker 容器配置,虚拟机等等。
周期性的执行这些命令。
sync 命令会更新 parity 信息,所有磁盘中修改的文件都会被读取,然后对应的 parity 信息都会更新。
touch 命令会将所有拥有 sub-second 时间戳的文件设置为 0,这样提高了 SnapRAID 识别移动和复制过的文件的能力,可以消除可能的重复。
scrub 命令会验证磁盘阵列中的数据和 sync
命令产生的 hash.
# Run this command for the first time
snapraid sync
# Run this command after the sync is completed
snapraid scrub
# Run this command for status
snapraid status
在安装完 OMV-Extras 后会在插件中看到两个相似的插件:
其主要区别就在于一个是合并文件夹,一个是合并硬盘。所以如果对于全新的硬盘,没有任何数据,可以直接利用 unionfilesystems 来将多块硬盘组合成一个 pool.