2024年8月

前言

参考SecretTeam安全团队的学习记录

什么是免杀?

免杀(Bypass AV, Anti-Virus Evasion)是指恶意软件通过各种手段规避杀毒软件和安全检测系统的识别和拦截,从而在目标系统中成功执行。这种技术不仅用于恶意软件的传播,也被信息安全研究人员用来测试和提升安全防护系统的能力。根据有无源码,免杀可以分为以下两种情况:

  • 二进制免杀

直接对一个二进制程序进行免杀,通过修改数据,加壳加花,定位修改特征码等等黑盒方式

  • 源码免杀

通过修改源码来进行免杀(大概率成为今后免杀的主流)

直接对一个二进制程序来进行免杀技术难度较高,免杀效果也不好,所以通常将二进制程序转换成一段shellcode,使用加载器来执行shellcode的方式来进行从无源码免杀向有源码免杀的转换,根据免杀阶段还分为以下两个阶段:

  • 静态免杀

静态免杀主要是为了抵抗杀毒软件的静态扫描,杀毒软件的静态扫描一般会通过提取文件中的一段特征串来与自身的病毒库中的特征码进行对比来判断该文件是否为恶意文件,因此我们一般围绕修改或是掩盖文件的特征码来实现静态免杀

  • 动态免杀

动态免杀主要是为了抵抗杀毒软件的动态查杀,如内存扫描,行为分析等

杀软特性

360

360有点全能了,在国内基本各个方面都是顶尖
报毒特征

HEUR/Malware.QVM06.Gen 一般情况下加数字签名可过
HEUR/Malware.QVM07.Gen 一般情况下换资源
HEUR/Malware.QVM13.Gen 加壳了
HEUR/Malware.QVM19.Gen 杀壳 (lzz221089提供 )
HEUR/Malware.QVM20.Gen 改变了入口点
HEUR/Malware.QVM27.Gen 输入表
HEUR/Malware.QVM18.Gen 加花
HEUR/Malware.QVM05.Gen 加资源,改入口点

QVM07加资源一般加到2M会报QVM06
再加数字签名,然后再慢慢减资源,这个方法对大部分木马有效果。
QVM06 加数字签名
QVM12杀壳
QVM13杀壳
QVM27杀输入表
QVM19 加aspack
QVM20就加大体积/加aspack压缩

金山毒霸

金山走的是云安全,云防护,云鉴定这些云安全路线,所以,断了网后金山就是个废

江民

主要是对特征码和资源进行定位查杀

瑞星

主要是主动防御查杀

卡巴斯基

非常变态的一款杀软,误报低,查杀率高,特征码+输入表变态查杀+静动态启发式+强力的虚拟机脱壳技术。人类已经无法阻止卡巴斯基的输入表查杀了,在反汇编下,你无论对输入表怎么重建,移位都不行,需要进行手动异或加密。

小红伞,木伞

小红伞的特征码定位抗干扰技术和启发式比较好

火绒

主要是主动防御这块,静态查杀也比较严格,不过很好做免杀

Windows Defender

静态查杀能力较强,动态查杀较强,监控 HTTP 流量

杀软的查杀方式

特征码

特征码是什么?特征码就是病毒分析师从病毒中提取的不大众化的不大于64字节的特征串。通过判断是否有这个特征字符串从而确定是否为病毒。通常为了减少误报,一个病毒会取数个特征码。举个例子,一款很出名的木马,它的名字就能被当作特征码。

静态启发式

静态启发式即对整个软件进行分析,杀软会指定一系列的规则,然后对软件进行扫描,当扫描出匹配这些规则的字符串等等之类时,杀软会将软件标记成怀疑对象,匹配到的规则越多,软件的可疑程度越高,到一定程度,就成了恶意软件了

动态启发式

动态启发式又叫虚拟机查杀技术,会模拟出一个近似于windows的系统,杀软将软件放入这个虚拟机运行,监测它的行为,如果操作越可疑,就越容易被定为病毒

HIPS

HIPS可以说是主动防御,何为主动防御,一个病毒或木马如果通过了表面查杀,那么主动防御就是最后一道防线,HIPS主要是对一个软件运行时的操作进行检测,如果发现软件有注册表操作,加载驱动这些一般程序不应操作的操作时,那么他就会以他R0级的优势,拦截掉并将程序暂停运行,也就是挂起,询问用户是否进行该操作。

云查杀

杀软那里有一套规则,如果一个软件触犯了这些规则,则杀软会上报至云服务器,到了云服务器后,则会对上报文件进行鉴定,可能会是人工鉴定,这样的效果比杀软查杀效果要好得多。那么如果分析出这个程序是病毒,那么就会将这个程序的MD5发生至云中心,用户在联网状态下杀毒的话,就与云中心核对MD5,如果对上了,无条件认定为病毒。相当于安装了杀软的所有用户给云上提供素材,一旦素材在一台电脑上被认定为病毒,则所有安装了该杀软的用户都会查杀这个素材

免杀中的术语

API

泛指Windows的API函数,Windows编程中的内容。Windows API是一套用来控制Windows的各个部件的外观和行为的预先定义的Windows函数,对Windows系统中的东西进行操作都会用到API,比如我移动个鼠标,点击个键盘都会有相对于的API函数

花指令

一段无意义的代码,用来迷惑杀软或则其它的反汇编工具,例如在汇编里:add eax, 1; sub eax, 1
就这种添加了代码,但对整体并无影响的代码就是花指令,当然我这里只是简单的示范,真正的花指令没有这么简单

输入表(导入表)

输入表就相当于EXE文件与DLL文件沟通的钥匙,形象的可以比喻成两个城市之间交流的高速公路,所有的导入函数信息都会写入输入表中,在PE 文件映射到内存后,Windows 将相应的DLL文件装入,EXE 文件通过“输入表”找到相应的DLL 中的导入函数,从而完成程序的正常运行,这一动态连接的过程都是由“输入表”参与的。

区段

PE结构中的区段,
.data .text
等等,保存代码,数据等等

加壳

加壳分为加压缩壳和保护壳〔加密壳〕压缩壳是目的是使程序变小,但没有防止被反破解的作用。保护壳恰恰相反,保护壳的目的是使程序尽量防止被反汇编,但好的保护壳会给程序植入大量垃圾代码,以干扰破解版者,所以程序会变大。

反启发

即加入对杀软的启发式干扰的代码

隐藏输入表

通过自定义API的方式隐藏导入表中的恶意API

混淆

这是一种将代码转换为难以理解的形式的技术,使得分析者难以从字节码中理解程序的逻辑。混淆可以应用于源代码、编译后的代码或二进制文件

代码注入

将恶意代码注入到合法程序中,使得恶意代码在合法程序的执行过程中被执行,从而避开杀毒软件的检测。

内存执行

恶意代码不在磁盘上留下痕迹,而是直接在内存中执行,这样可以减少被杀毒软件扫描到的机会

文件加密

将恶意文件加密存储,只有在执行时才解密,这样可以避免杀毒软件通过文件内容进行检测。

多态

生成多个变种的恶意代码,每种变种都有不同的特征,使得杀毒软件难以通过单一的特征码来识别。

行为混淆

通过改变程序的行为模式,使得恶意行为看起来像是正常行为,从而避开基于行为分析的检测

0day

利用操作系统或应用程序的0day漏洞来执行恶意代码,杀毒软件无法检测到

签名

使用合法的数字证书签名恶意软件,以欺骗用户和杀毒软件,使其看起来像是可信的软件

沙箱逃逸

沙箱是一种检测恶意软件的环境,它模拟了一个安全的执行环境。沙箱逃逸技术是指恶意软件能够检测到自己是否在沙箱中运行,并在检测到沙箱时改变行为,以避免被检测。

利用系统服务

通过操作系统服务来执行恶意行为,因为系统服务通常具有较高的权限,可以绕过一些安全措施

利用云服务

将恶意代码或数据存储在云服务上,通过云服务来分发或执行恶意行为,这样可以分散风险并增加检测难度

杀软报毒命名规则

杀毒软件的报毒基本遵循一套原则,就是
CARO
原则,由反病毒专家联盟 CARO提出,遵循以下格式:
<威胁类型>.<平台>.<恶意软件系列>.<变体>.<其他信息*>
卡巴斯基在此基础上添加了前缀:

[前缀:]<威胁类型/行为>.<平台>.<恶意软件家族><.变体><其他信息>

前缀

该前缀标识检测到该对象的子系统。
前缀
HEUR:
用于表示启发式分析器检测到的对象;
前缀
PDM:
用于表示主动防御模块检测到的对象。
前缀不是全名的必需部分,并且可能不存在。

行为

威胁类型/行为代表主要威胁类别,描述威胁的主要行为是什么

  1. 对于恶意软件:Trojan(木马)、Worm(蠕虫)、Virus(病毒)、Ransomware(勒索软件)、Coinminer(挖矿) 和Backdoor(后门)是我们最常见的威胁类型。
  2. 对于灰色软件:Adware广告软件、Spyware间谍软件和 PUA 是最常见的威胁类型。


平台

通常指win32,x64,linux,mac os

家族

用于表示一组具有相同来源(作者、源代码)、操作原理或有效负载的检测到的对象。每个家族都是根据其表现的行为来命名的。常见的就是:Generic、Infector、AntiAV、KillFiles等。

变体

为了识别一个家族中不同恶意软件的变体,字母按顺序使用并称为变体,从“.a”开始:“.a”-“.z”、“.aa”-“.zz”等

报毒案例

HEUR:Worm.[Platform].Generic
此分类涵盖的对象在远程计算机上运行搜索,并尝试将自身复制到读/写可访问目录、使用操作系统功能搜索可访问网络目录和/或对计算机进行随机搜索。
[Platform] 字段可以是“Script”或“Win32”。


HEUR:Virus.[Platform].Generic
此分类涵盖的对象会在受害计算机的本地资源上创建自身的副本。
[Platform] 字段可以是“Script”或“Win32”。


HEUR:Email-Worm.[Platform].Generic
此分类涵盖的对象尝试以电子邮件附件的形式发送自身的副本,或者作为位于网络资源上的自身文件的链接。
[Platform] 字段可以是“Script”或“Win32”。


HEUR:Virus.[Platform].Infector
此分类涵盖的对象在计算机中搜索文件并将一系列信息写入这些文件。例如,这样的对象可以将其主体写入可执行文件或写入包含指向具有 .html、.php、.asp 和其他扩展名的文件的链接的 HTML 代码。
[Platform] 字段可以是“Script”或“Win32”。


PDM:Worm.Win32.Generic
此分类涵盖的对象搜索远程计算机网络并尝试将自身复制到读/写可访问目录、使用操作系统功能搜索可访问网络目录和/或对计算机进行随机搜索。

引言

在工作中我经常需要处理一些复杂、动态表单,但是随着需求不断迭代,我们也许会发现曾经两三百行的
.vue
文件现在不知不觉到了两千行,三千行,甚至更多...

这对于一个需要长期维护的项目,无疑是增加了很多难度。

因此,为了减小文件大小,优化表单组织的结构,我在日常的开发中实践出一种基于组件的表单拆分方法,同时还能保证所有的表单项是处于同一个
el-form
中。

这对于一个一开始就没有做好文件组织,组件化的项目,有以下几个优点:

  1. 改动小!后续新增表单项基本不会改动以前的代码
  2. 基于组件!在逻辑上对表单项做出拆分,并在任何地方嵌入
  3. 易维护!化单个大组件为多个小组件,每个组件只专注于一部分表单。

表单拆分

接下来我们会通过完成一个实际表单的方式来介绍如何实践这种表单组织方式。


element-ui
文档中的这个
表单
为例,接下来尝试用我们的方式来实现

首先假设我们当前有一个
vue
文件
./form/myForm.vue

<template>
  <el-form ref="form" :model="form" label-width="140px">
  ...
  </el-form>
<template>
<script>
export default {
 name: 'myForm',
 data() {
  return {
   form: {}
  }
 }
}
</script>

如果我们直接按照
element-ui
的表单文档来写,那么我们的
myForm.vue
文件可能就会变成这样:

<el-form ref="form" :model="form" label-width="80px">
  <el-form-item label="活动名称">
    <el-input v-model="form.name"></el-input>
  </el-form-item>
  <el-form-item label="活动区域">
    <el-select v-model="form.region" placeholder="请选择活动区域">
      <el-option label="区域一" value="shanghai"></el-option>
      <el-option label="区域二" value="beijing"></el-option>
    </el-select>
  </el-form-item>
  <el-form-item label="活动时间">
    <el-col :span="11">
      <el-date-picker type="date" placeholder="选择日期" v-model="form.date1" style="width: 100%;"></el-date-picker>
    </el-col>
    <el-col class="line" :span="2">-</el-col>
    <el-col :span="11">
      <el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;"></el-time-picker>
    </el-col>
  </el-form-item>
  <el-form-item label="即时配送">
    <el-switch v-model="form.delivery"></el-switch>
  </el-form-item>
  <el-form-item label="活动性质">
    <el-checkbox-group v-model="form.type">
      <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
      <el-checkbox label="地推活动" name="type"></el-checkbox>
      <el-checkbox label="线下主题活动" name="type"></el-checkbox>
      <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
    </el-checkbox-group>
  </el-form-item>
  <el-form-item label="特殊资源">
    <el-radio-group v-model="form.resource">
      <el-radio label="线上品牌商赞助"></el-radio>
      <el-radio label="线下场地免费"></el-radio>
    </el-radio-group>
  </el-form-item>
  <el-form-item label="活动形式">
    <el-input type="textarea" v-model="form.desc"></el-input>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="onSubmit">立即创建</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>
<script>
  export default {
    data() {
      return {
        form: {
          name: '',
          region: '',
          date1: '',
          date2: '',
          delivery: false,
          type: [],
          resource: '',
          desc: ''
        }
      }
    },
    methods: {
      onSubmit() {
        console.log('submit!');
      }
    }
  }
</script>

假设我们还需要为这个表单增加审批流程,例如文档中的这个
表单
在加入新的表单项后,可能目前看着还好,但是随着表单项越来越多,这个文件会变得越来越大,越来越难以维护。所以我们尝试将这个表单项拆分为单个组件,模拟我们维护一个超大表单的场景。

新增子表单项组件

我习惯在当前表单的目录下,创建一个
components
目录,然后在
components
目录下创建一个
audit
目录,并在
audit
目录下创建一个
index.vue
文件,用于存放审批流程相关的组件。如果后续有一些只有
audit/index.vue
文件中才用到的组件,我也会放在
audit
目录下。保持目录结构清晰。

<template>
  <div class="audit-form-item">
    <el-form-item label="审批人" :prop="`${propPrefix}.user`">
      <el-input v-model="form.user" placeholder="审批人"></el-input>
    </el-form-item>
    <el-form-item label="活动区域" :prop="`${propPrefix}.region`">
      <el-select v-model="form.region" placeholder="活动区域">
        <el-option label="区域一" value="shanghai"></el-option>
        <el-option label="区域二" value="beijing"></el-option>
      </el-select>
    </el-form-item>
  </div>
</template>
<script>
export const auditFormData = () => ({
  user: '',
  region: ''
})

export default {
 name: 'auditFormItem',
 props: {
  value: {
   type: Object,
   default: () => auditFormData()
  },
  propPrefix: {
   type: String,
   default: ''
  }
 },
 data() {
  return {
   form: this.value
  }
 },
 watch: {
  value(newVal) {
   this.form = newVal
  },
  form(newVal) {
   this.$emit('input', newVal)
  }
 }
}
</script>

因为
element-ui
在对表单进行校验时,实际上是对
model
上绑定的数据进行校验,所以为了能够对数据正确执行校验,我们需要在
auditFormItem
组件中实现
v-model
指令。

auditFormItem
组件的
propPrefix
属性用于指定表单项的前缀,便于我们在嵌入到
el-form
中时,能够正确绑定表单项的
prop
属性。

auditFormData
函数返回了当前表单项的默认数据。父组件通过执行该函数,可以对子表单执行正确的初始化。不仅如此,通过这种方式,我们将每个子表单项的数据和组件绑定在一起,避免了父组件data中出现大量表单项数据,导致难以维护的问题。每个子表单维护各自的数据,互不干扰。

如何嵌入已有项目

接下来我们尝试将
auditFormItem
组件嵌入到
myForm.vue
文件中

<template>
  <el-form ref="form" :model="form" label-width="140px">
    <!-- 其他表单项 -->
    <!-- ... -->
    <audit-form-item v-model="form.audit" propPrefix="audit"></audit-form-item>
  </el-form>
</template>
<script>
import auditFormItem, { auditFormData } from './components/audit/index.vue'
export default {
  components: {
    auditFormItem
  },
  data() {
    return {
      form: {
        audit: auditFormData()
      }
    }
  }
}
</script>

如何进行校验

经过上面的操作,我们实现了将一个表单拆分为多个子表单项,那么如何进行表单校验呢?

我们知道在
element-ui
中,要对一个表单项进行校验有两种方式:

一种是在
el-form
上绑定
rules
属性,它会通过
prop
进行索引,自动对绑定的表单项进行校验。
另一种是在
el-form-item
上绑定
rules
属性,这会对单条表单项进行校验。

出于我们拆分表单项的场景,我们选择第二种方式,在
el-form-item
上绑定
rules
属性,然后在各个子组件中维护
rules
。如果有一些通用的校验规则,我们也可以在
audit/validate.js
文件中进行维护,然后通过
import
的方式引入。

如何处理联动校验

在复杂表单中,我们可能需要对多个表单项进行联动校验,例如:实时校验表单项的合法性,当
form.region

shanghai
时,
form.user
不能为空,当
form.region

beijing
时,
form.user
必须为空。

那么如何处理这种联动校验呢?

我们可以在
el-form
所在的组件中,定义一个validate方法,通过
element-ui
提供的
validateField
方法,对特定进行校验。

<audit-form-item v-model="form.audit" propPrefix="audit" @validate="validate"></audit-form-item>

methods: {
  validate(fields) {
    this.$refs.form.validateField(fields)
  }
}

然后在
audit/index.vue
文件中,我们可以通过
$emit('validate', fields)
方法,对当前表单项进行校验。

如何处理跨组件的联动校验

在我们拆分表单为多个子组件后,还可能会出现不同子组件之间的联动校验,例如:当子组件1中的
form.region

shanghai
时,子组件2中的
form.user
不能为空。
对于这个问题,其实我目前还没有想到很好的解决方案,当前是在
el-form
所在的组件中,定义额外的校验规则,然后绑定到一个空白的
el-form-item
上。

<el-form-item label="" label-width="0" prop="_form_validate_"></el-form-item>

rules: {
  _form_validate_: {
    validator: (rule, value, callback) => {
      // 联动校验逻辑
    }
  }
}

也可以考虑用vuex来维护需要跨组件共享的数据。

const crossCmpConfig = {
  state: {
    region: '',
  },
  mutations: {
    UPDATE: (state, { key, val }) => {
      state[key] = val
    },
  },
}

export default crossCmpConfig

多层嵌套

基于上面的方式,很容易就能想到,我们甚至可以继续在
audit/index.vue
文件中,继续嵌入别的子组件,例如:
audit/audit-info/index.vue
, 然后通过相同的方式,继续嵌入到
audit/index.vue
文件中。

结语

上面就是我日常开发中,处理复杂表单的一些经验总结,希望对大家有所帮助。

随着使用 JuiceFS 的时间越来越长,一些用户已经用多种数据库和对象存储创建了很多的 JuiceFS 文件系统。有些是纯云端的,有些是纯本地的,有些则是本地与云端结合的。它们当中有一些是存储了文件的,而有一些则只是测试目的临时创建的。多个文件系统混合在一起使用难免会混淆,特别是在同一个数据库实例中创建多个文件系统时会更为明显。

比如在一个 Redis 实例的 0 号数据库和 1 号数据库都创建了文件系统,其中一个是正常使用的,另一个是测试用的,当需要删除测试用的文件系统时,就可能会误删正常使用的文件系统。

在这篇文章会分享一些解决类似问题的日常管理技巧,希望能够帮助到更多的 JuiceFS 用户。

前置知识

在开始之前,先介绍一些 JuiceFS 的基本概念,以便大家更好地理解这些技巧。

如下图,JuiceFS 采用的是一种数据与元数据分离存储的技术架构,数据存储在对象存储中,元数据存储在数据库中。

从管理的角度来说,这就涉及到数据库和对象存储两个部分的管理。

情景一:数据库被 JuiceFS 使用了吗?

如果你也像我一样喜欢尝试用不同的数据库作为 JuiceFS 的元数据引擎,那么你可能会遇到这样的情况:有很多数据库,但不确定哪个数据库被 JuiceFS 使用。

对于这种情况,无外乎两种判断方法:

  1. 用 JuiceFS 客户端执行检查;
  2. 用数据库客户端执行检查。

两种方法都能用来判断,只是有些数据库更适合用第一种方法,有些数据库更适合用第二种方法。

适合用 JuiceFS 客户端检查的数据库

对于 SQLite3、Badger 这样的单机数据库,直接用 JuiceFS 客户端检查更合适。因为一个数据库只对应一个 JuiceFS 文件系统,所以只要 JuiceFS 客户端能够连接到数据库,就能够检查出这个数据库是否被 JuiceFS 使用。

例如,在我本地电脑的某个目录中发现了一个名为 my.db 的文件,我现在不确定它究竟是 JuiceFS 文件系统的元数据,还是其他应用的数据库。这时,我可以使用 JuiceFS 客户端的 status 子命令来检查:

juicefs status sqlite3://my.db

从命令输出的 JSON 中,包含文件系统的名称、UUID、存储类型、对象存储等信息,可以确定这个数据库是一个 JuiceFS 文件系统的的元数据引擎。

如果没有输出 JSON 信息,并显示 database is not formatted,那么这个就不是 JuiceFS 使用的元数据引擎。

与 SQLite3 类似,Badger 也是一个单机数据库,但它的数据库不是单个文件,而是一个目录。

比如,我在本地发现一个名为 myjfs 的目录,时间久远已经不记得它究竟是一个普通的目录,还是 badger 数据库目录。这时,我可以使用 JuiceFS 客户端的 status 子命令来检查:

juicefs status badger://myjfs

适合用数据库客户端检查的数据库

虽然完全可以用 JuiceFS 客户端进行检查,但在数据库很多,以及记不得有哪些数据库的情况下,用数据库客户端检查网络数据库会更为直观方便。

Redis

默认情况下,一个 Redis 实例有编号为 0~15 共计 16 个数据库。如果之前没有在备忘本中明确记录每个数据库的用途,那么时间久了再次要用到的时候,就很难区分哪个数据库是 JuiceFS 使用的。

比如,我有以下 Redis 实例:

地址 192.168.1.88
端口 6379
密码 password

我不确定哪个数据库用于 JuiceFS,甚至不确定是否有数据库用于 JuiceFS。这时,最简单的办法就是使用 redis-cli 客户端连接到 Redis 实例,逐一对每个数据库执行检查:

# 连接到 Redis 实例
redis-cli -h 192.168.1.88 -p 6379 -a password

# 检查哪些数据库存储了数据
192.168.1.88> info keyspace

通过 info keyspace 命令,可以看当前实例的 db0、db1 和 db3 都存储了数据,接下来就可以依次对这些数据库执行 get setting 检查是否是 JuiceFS 使用的数据库。

如图所示,db0 没有 JuiceFS 的信息,说明它不是 JuiceFS 使用的数据库。db1 和 db3 都包含 JuiceFS 的信息,说明它们是 JuiceFS 使用的数据库。

Postgres、MySQL、MariaDB

对于这三种数据库,有很多图形化客户端工具可以直接使用,比如 pgAdmin、Adminer、Navicat 等,可以直观地显示数据库中的表、数据等信息。

笔者相对更喜欢使用 Adminer,它是一个非常轻量级的数据库管理工具,可以直接通过 Docker 部署,通过浏览器访问,同时支持上述三种数据库。

假设在本地电脑上已经安装了 Docker,那么可以通过以下命令来部署 Adminer:

docker run -d -p 8080:8080 --name adminer adminer

部署完成后就可以通过浏览器访问
http://localhost:8080
来使用 Adminer。

以 Postgres 数据库为例,连接到 Postgres 数据库后,可以看到数据库中的表、数据等信息。

在数据库列表中,如果不确定哪个数据库是 JuiceFS 使用的,可以逐一点击进入数据库,查看其中的表。

如下图,JuiceFS 的表名称通常是以 jfs_ 开头的。

MySQL、MariaDB 等数据库的操作类似,访问时只需要在 Adminer 登录界面中据实选择即可。

另外,JuiceFS 还支持其他数据库,比如 TiKV、etcd、FoundationDB 等,检查它们是否用于 JuiceFS 的方法都是类似的,这里不再赘述。

情景二:对象存储被 JuiceFS 使用了吗?

对于 JuiceFS 文件系统来说,元数据引擎记录着所有文件的信息,对象存储则是保存着所有实际的文件。二者相辅相成,缺一不可。

从管理的角度来说,只要能够确定 JuiceFS 文件系统的元数据引擎,就能够确定对应的对象存储。

也就是说,只要用 juicefs status 命令扫一下相应的数据库,就能找到这个文件系统关联的对象存储。

根据输出的信息,在 Bucket 部分可以找到它使用的对象存储。当然,如果你在同一个云平台上有多个账号,最好还是从云平台的文件管理器中逐一查看 Bucket 内容。

如图所示,JuiceFS 会在 Bucket 根目录下创建一个与文件系统同名的文件夹作为根目录。打开这个目录,可以看到名为 juicefs_uuid 的文件,它是识别 JuiceFS 文件系统的关键。

另外,在文件系统的根目录下,通常还会有 chunks 和 meta 两个目录,分别存储文件的数据块和元数据备份。通过这些特征,就可以判断一个 Bucket 是否属于 JuiceFS 文件系统。

总结

以上两个场景分别介绍了如何判断已有的数据库和对象存储是否被 JuiceFS 使用,掌握了这些技巧,相信读者可以更好地管理 JuiceFS 文件系统,避免误删、误操作等问题。

文章的最后再提供几个创建 JuiceFS 文件系统时的建议:

  1. 使用单机数据库作为元数据引擎时,数据库命名尽量有意义和简短。这样既方便识别,又方便后续使用;
  2. 预计会长期使用的文件系统,数据库和 Bucket 建议让 JuiceFS 独享,尽量不要与其他应用共享使用,避免潜在的误操作和使用冲突;
  3. 为文件系统定义一个容易识别的名称,有助于后续管理,比如在名称中添加 jfs 词缀 my-jfs、test-jfs 等。

如果你有其他问题,欢迎加入 JuiceFS 微信群进行提问,如果你有其他管理技巧,也欢迎分享给我们。

最近加了很多新人朋友,大部分都是初级开发者。都想要加入开发者群交流讨论平时遇到的问题。新人朋友有这种想法其实蛮好的,但是很多人似乎都不知道如何有效提出自己的问题,他们往往一张图或者一句话就往群里一扔,并且希望能有群友给自己解答。

大部分情况这种问题都会石沉大海,少部分情况下会有一些热心肠的群友会指出他们的问题描述不够有效,进而 引导他完善自己的问题。可后来新人越来越多,这种新手问题似乎总是充斥在群里,慢慢得,没有人愿意在给新人做解答了。

再举个例子: 我是一个技术群群主,看到了一个新手提出了不知头尾的问题,我起初热心教导他得先完善问题描述,并且耐着性子引导他解决问题,然后他一句话也没说消失在了群里,最后我发现他似乎是得到问题答案后就退了群。大家会觉得我怎么想,我以后还会愿意帮助这些新手嘛?

上面这个例子虽然比较极端,但是我想表达得是提问者作为被帮助得一方,最基本得道德底线就是对帮助自己的人持有礼貌得态度。同为开发,我本无私,不求回报,没想到你却如此绝情!

自人类社会诞生以来,人情世故就充斥其中。

好啦,上面说完了,我写这篇文章得目的就是想在 it 领域告诉那些不擅长向别人提问得开发者,如何做一个优雅(礼貌且有效)得提问者。接下来,我会按照为什么要提问、自我解决、礼貌且有效的提问这三个部分来给大家讲解。

为什么要提问

自参加九年义务教育起,老师通常都会在上完课后问底下得同学们,大家还有什么问题吗?

读书时,有些同学可能一年也提不出一个问题,有些同学则几乎每节课都会提出问题,若干年后,哪些提问较多得同学大部分都比不爱提问题得同学混得好。

在开始学习如何提问之前,我们需要理解为什么提问如此重要:

  • 加速学习过程
    : 通过提出正确的问题,可以更快地理解复杂的概念和技术。
  • 解决实际问题
    : 在遇到技术难题时,有效的提问可以帮你更快地找到解决方案。
  • 建立专业网络
    : 高质量的问题可以吸引同行的注意,帮助你建立专业人脉。
  • 提高沟通能力
    : 学会清晰表达技术问题,这项技能在职业生涯中至关重要。

提问题不仅是为了解决问题,也是体现你的专业能力以及沟通能力,你不问是没人会主动来问你的!在这里送给大家一句话 “勇敢的人先享受世界”。

尝试自我解决

在向他人寻求帮助之前,最好先自己在网上搜索一番,毫不夸张得说,it 领域中大部分人遇到得问题在网上都有 现成得答案。只是大家可能没有掌握正确得搜索方法,导致得不到想要得答案。

所以在大家自我解决问题之前,我得先给大家讲讲怎么搜索问题!

怎么搜索问题

搜索引擎

在国内最常用的就是百度了,大部分人都知道百度的存在,那为什么在得到控制台一串报错后,不舍得喂给百度,而是丢到群里一大串 error log?有条件的开发者也可以使用谷歌搭配英文搜索,得到的搜索结果会更准确一些。

官方文档

大部分热门的语言、框架、中间件都提供了官方文档,官网通常是最权威、最新的信息来源,并且许多项目的官网中都有 FAQ 部分,里面包含了最常见的问题和解答,浏览这些,就可以解决大部分人的疑问。

浏览相关论坛和社区

如 Stack Overflow, GitHub Issues, Reddit 等平台上可能已经有人问过类似的问题。如果有,那么评论区里往往就包含了现成的答案。

通常情况下我搜索问题的优先级就是按照搜索引擎 -> 官方文档 -> 浏览相关论坛和社区。大部分问题都可以在前两步找到答案。只有当我遇到框架、中间件中一些罕见的报错时,我才需要在 Github Issues 区寻找相似问题是否已经有人提出以及解决方案。

复现和分析问题

复现问题

首先检查是必现问题还是偶现问题,这有助于别人帮你解决问题时,节约时间。

分析问题

查看错误消息、日志、堆栈跟踪等信息。大部分框架、语言返回的错误信息实际上已经告诉了你们问题产生的原因。只不过现代操作系统起源与国外,大部分编程语言的关键词都是英文。这对于国人新手学习一门编程语言确实是一种阻碍,相比之下,隔壁阿三把英文作为官方语言,在 it 领域确实比国内人更有优势一些。

幸运的是,现如今各种翻译软件层出不穷,对于大部分编程语言的报错,你只需要动动手把这一段英文翻译一下就能知道这个问题根源是什么,进而联系代码查看问题出在哪里。

优雅的提问

当你自己确实解决不了一个问题时,那么是时候该向别人请教了。虽然我建议能自我解决的问题就尽量自我解决,但是考虑到时间成本和实现成本,我建议大家给自己设置一个 end time。当超过这个时间还是解决不了又或者实现成本巨大的话,就直接去寻求外部帮助把,这一点也不丢人,因为你也付出了努力。

有效的问题

问问题谁不会啊,但是大家要清楚,问题是你提的,你当然知道这个问题的产生背景、上下文、代码等。但是对于被提问者,他知道这些吗?你有考虑过被提问者能第一时间理解你提的问题吗?对于一些不知道头尾、一句话描述的问题,被提问者有必要为你解答吗?

接下来我通过两个真实例子告诉大家什么是有效的问题什么是无效的问题。

无效的问题 1

image

这是群友某日在群里发的一个问题,上来就是一段经典的英文 log。随后问怎么解决?说实话,对于这种问题我都是默认不予理睬的,这个问题激发不了我得助人欲。我来讲讲我为什么。

  • 问题背景:这个日志在在什么环境在产生?是 Java、Javascript 还是 Python 又或者是浏览器。
  • 提供上下文 : 解释你试图实现什么,以及在哪一步遇到了问题。
  • 可复现的代码:提供足够重现问题的最少代码。
  • 礼貌用语:寻求帮助的话得保持礼貌。而不是一段 log 接一句怎么解决。别人凭什么给你解决?你连最基本得礼貌都没有。

看了我不予理睬得原因后,大家应该也能理解我了把。那么怎么把这个无效得问题变成一个有效得问题嘞?我在用搜索引擎直接搜索这段 log 后得到了问题得上下文以及复现代码(这个问题别人早已经遇见过了,并且提供了解决方案。。。)后,重新编写了一个有效得问题如下,

有效的问题 1

image

对比无效问题 1 大家可以看到我提出得问题有哪些改变吗?

  • 问题背景:前端开发在浏览器 chrome 控制台有报错。
  • 提供上下文 :使用 element ui 提供的单选框组件 el-radio-gruop 时,点击单选框报错。
  • 可复现的代码:问题代码已经提供。
  • 礼貌用语:开头第一句话就是大佬们,满满得求知欲。

无效的问题 2

image

这是某日群友在技术群直接 at 我发的一张图,问我 xxl-job 后台账号密码多少,我在仔细看完他发的图后,寻思 xxl-job 后台的登录框也不长他图里这样啊!我怎么给你回复嘞?后面的对话更像是加密通话一般,
image
image
image

在这个问题里,提问者一开始就缺失了问题背景、上下文,最后一句我知道啥问题了,结束了话题收了场,对他而言相当于自己灵光一闪,迅速解决了问题,但是对于那些想帮他解决问题的人而言只能说是莫名其妙。

我在尝试理解他这个问题得背景、上下文后,得出的结论是,可能他一开始就把 xxl-job 后台与其他系统后台地址搞错了,登录其他系统后台时输入 xxl-job 后台的账号密码就登不进去了,其实就是一个乌龙。那么这种乌龙问题能不能变成一个有效的问题,让大家不在感觉莫名其妙?

有效的问题 2

image

对比无效问题 2 得改变如下,

  • 问题背景:xxl-job 后台登陆失败
  • 提供上下文 :后台地址
    http://localhost:8081/login,使用
    xxl-job 默认账号密码 admin/123456
  • 可复现的代码:这个问题暂不不需要。
  • 礼貌请教:开头第一句话就是大佬们,满满得求知欲。

总结

OK,本文到这里也就结束了,针对如何做一个优雅得提问者这个话题,我来给大家做一个总结。

  1. 遇到问题不要慌,善用搜索引擎、官方文档、技术论坛社区如 Github 等,查找是否有现成答案。
  2. 分析问题原因,遇到错误时分析错误日志,联系代码,尝试定位问题原因,自我解决。
  3. 提出问题时要携带背景、上下文信息,便于他人理解,礼貌得寻求他人帮助,不要吝啬谢谢两个字。

如果觉得这篇文章写的不错的话,可以关注我的公众号【程序员wayn】,第一时间更新更多技术干货、项目教学、经验分享的文章。

Blazor 组件

基础

新建一个项目命名为 MyComponents ,项目模板的交互类型选 Auto ,其它保持默认选项:
image

客户端组件
(Auto/WebAssembly):
最终解决方案里面会有两个项目:
服务器端项目

客户端项目
,组件按存放项目的不同可以分为以下两种组件:

  • 服务器端组件
    :


    • 主要用于服务器端渲染(SSR)
    • 被放置在服务器端项目中
    • 适用于不需要实时交互或复杂用户交互的场景
  • 客户端组件
    (Auto/WebAssembly):


    • 组件位于客户端项目内
    • 使用 WebAssembly 技术进行编译,能够直接与浏览器交互
    • 适合需要交互性和实时更新的应用场景
    • 使用 SignalR 可以实现实时通信,从而增强组件的功能性

两种组件选择原则如下:

  • 如果组件不需要交互性,将其作为
    服务器端渲染
    的组件。
  • 如果组件需要交互性(例如响应用户的输入、实时数据更新等),则应该考虑将其作为
    客户端组件
    ,可以利用 SignalR 提供的实时通信功能。

在客户端项目中新建一个 Demo 组件:

<!-- 选择 Auto 或 WebAssembly ,否则无法交互 -->
@rendermode InteractiveAuto

<h3>Demo</h3>

<!-- 文本不为空时才显示标签 -->
@if (textInfo is not null)
{
    <h4>Info: @textInfo</h4>
}

<!-- 按钮样式参考 Counter 组件 -->
<button class="btn btn-primary" @onclick="UpdateText">Update Text</button>
<!-- 委托方式调用方法,可以传入参数 -->
<button class="btn btn-primary" @onclick="(()=>{UpdateNumber(10);})">Update Number</button>


@code {
    private string? textInfo = null;

    private void UpdateText()
    {
        textInfo = "This is the new information";
    }

    private void UpdateNumber(int i = 0)
    {
        textInfo = $"This is number {i}";
    }
}

在服务器端项目中的 Home 页面中引用 Demo 组件:

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<Demo />

在 _Imports.razor 中引用 Demo 组件的命名空间:

@using MyComponents.Client.Pages

路由导航

在客户端项目中添加一个 Start 组件,razor 代码如下:

@*组件可以同时有多个路由*@
@page "/page"
@page "/pages/start"

@*可以使用组件名作路由*@
@attribute [Route(nameof(Start))]

@*页面跳转必须指定交互性,并注入导航管理器*@
@rendermode InteractiveAuto
@inject NavigationManager Navigation

<h3>Start</h3>

@*通过NavigationManager.NavigateTo方法跳转到Counter组件*@
<button class="btn btn-primary" onclick="@(()=>Navigation.NavigateTo(nameof(Counter)))">Go to Counter</button>

@*执行完整的页面重新加载*@
<button class="btn btn-primary" onclick="@(()=>Navigation.Refresh(true))">Refresh</button>
@code {

}

上面的代码演示了如何使用路由和导航管理器进行页面跳转,
组件可以同时有多个路由,也可以使用组件名作路由

跳转到其它组件时会用到
增强导航
,参考
增强的导航和表单处理

参数

组件参数

在客户端项目中添加一个 BetterCounter 组件,razor 代码如下:

@rendermode InteractiveAuto

<h3>BetterCounter</h3>
<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {

    //将目标成员表示为组件参数
    [Parameter]
    public int CurrentCount { get; set; }

    private void IncrementCount()
    {
        CurrentCount++;
    }
}

组件参数将数据传递给组件,使用组件类中包含 [Parameter] 特性的公共 C# 属性进行定义
,参考
组件参数

单向绑定

使用组件时需要将目标成员作为组件参数传递给组件,如在 Home 页面中引用 BetterCounter 组件:

<BetterCounter CurrentCount="100" />
<!-- 或 -->
<BetterCounter CurrentCount=@currentCount />

路由参数

路由器使用路由参数
以相同的名称填充相应的组件参数,路由参数名不区分大小写
,参考文档
路由参数

在 BetterCounter 组件的 razor 代码中添加如下路由:

@*路由参数(无约束)*@
@page "/BetterCounter/{CurrentCount}"

@*路由参数约束*@
@page "/BetterCounter/{CurrentCount:int}"

@*可选路由参数(与上面任意一个路由搭配使用实现可选效果)*@
@page "/BetterCounter"

为了实现可选路由参数,需要在组件中添加一个默认值:

//将目标成员表示为组件参数
//需要将参数属性的类型更改为可为 null,这样就可以分辨出它是否被指定了值
[Parameter]
public int? CurrentCount { get; set; }

//为可选参数指定默认值    
protected override void OnInitialized()
{
    base.OnInitialized();    
    CurrentCount = CurrentCount ?? 1;
}

添加一个 Titlet 成员来接受查询字符串的参数:


//指定组件参数来自查询字符串
//路由:/BetterCounter?Titlet=asd
[SupplyParameterFromQuery]
public string? Titlet { get; set; } = "BetterCounter";
  • 路由参数
    :在添加组件的
    @page
    声明时,通过将路由参数的名称括在一对
    { 大括号 }
    中,在URL中定义了路由参数,参考示例
    路由参数

  • 路由参数约束
    :以冒号为后缀,然后是约束类型,约束类型参考文档
    路由约束
    。以路由
    /BetterCounter/abs
    为例:


    • 没有路由约束时会报类型转换异常(字符串无法转为int类型)
    • 有路由约束时会显示404错误(没有匹配到该 URL)
  • 可选路由参数
    :Blazor
    不明确支持可选路由参数
    ,但通过在组件上添加多个
    @page
    声明,可以轻松实现等效的路由参数,参考文章
    可选路由参数

  • 查询字符串
    :使用
    [SupplyParameterFromQuery]
    属性指定组件参数来自查询字符串,更多应用场景参考文档
    查询字符串

生命周期事件

以下简化图展示了 Razor 组件生命周期事件处理,参考文档
生命周期事件

image

在 Counter 组件中添加日志记录,观察组件的生命周期:

@inject ILogger<Counter> log

//...

@code {
    //...
    protected override void OnInitialized()
    {
        log.LogInformation($"Initialized at {DateTime.Now}");
    }
    protected override void OnParametersSet()
    {
        log.LogInformation($"ParametersSet at {DateTime.Now}");
    }
    protected override void OnAfterRender(bool firstRender)
    {
        log.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
    }
}
  • 组件初始化 (OnInitialized{Async})
    :专门用于在组件实例的整个生命周期内初始化组件,参数值和参数值更改不应影响在这些方法中执行的初始化。

  • 设置参数之后 (OnParametersSet{Async})
    :在 OnInitialized 或 OnInitializedAsync 中初始化组件后或父组件重新呈现并变更参数时调用。

  • 组件呈现之后 (OnAfterRender{Async})
    :OnAfterRender 和 OnAfterRenderAsync 组件以交互方式呈现,并在 UI 已完成更新(例如,元素添加到浏览器 DOM 之后)后调用。OnAfterRender 和 OnAfterRenderAsync 的
    firstRender
    参数:


    • 在第一次呈现组件实例时设置为 true。
    • 可用于确保初始化操作仅执行一次。

运行后导航到Counter界面,控制台输出如下:
image

状态更改

StateHasChanged
通知组件其状态已更改
。 如果适用,
调用 StateHasChanged 会导致组件重新呈现

将自动为 EventCallback 方法调用 StateHasChanged,也可以根据实际需求在组件中手动调用 StateHasChanged:

private async void IncrementCount()
{
    currentCount++;
    await Task.Delay(1000);
    StateHasChanged();

    currentCount++;
    await Task.Delay(1000);
    StateHasChanged();

    currentCount++;
    await Task.Delay(1000);
    StateHasChanged();
}

上面的代码,如果不调用 StateHasChanged 则点击后只会显示 1 ,用 StateHasChanged 后则会依次显示 1 2 3 。

组件事件

嵌套组件的常见方案是
在发生子组件事件时在父组件中执行某个方法
(如子组件中的 onclick 事件),跨组件公开事件请使用
EventCallback
,父组件可向子组件的 EventCallback 分配回调方法。

在 Counter 组件中添加一个事件:

@code {
    private int currentCount = 0;

    //定义一个事件回调参数
    [Parameter]
    public EventCallback<int> OnCounterChange { get; set; }

    private async Task IncrementCount()
    {
        currentCount++;
        //触发事件回调
        await OnCounterChange.InvokeAsync(currentCount); 
    }
}

在另一个客户端组件为 Counter 组件的事件分配回调方法(服务端组件会报错):

<Counter OnCounterChange="UpdateCounter" />

@code {
    private int currentCount = 0;
    private void UpdateCounter(int val)
    {
        currentCount = val;
    }
}