使用 k6 做一次负载性能测试

负载测试(性能测试,压力测试)是一个较为复杂的任务,包括了测试目标,工具开发,脚本开发,CI 集成,结果分析,性能调优等等部分,可以衡量服务是否是一个高可用,高性能的服务。负载测试能检验在不同的工作负荷下,服务的硬件消耗和响应,从而得到不同负载情况下的性能指标。压力测试能检验软硬件环境下服务所能承受的最大负荷并帮助找出系统瓶颈所在。

k6 是什么

k6 是用 Go 语言编写的一种高性能的负载测试工具。

k6 具有下面几个特点

  • 可配置的负载生成(Configurable load generation),低配置的机器就可以生成大量流量
  • 利用代码进行测试(Tests as code),重用测试脚本,模块化逻辑,版本控制,和 CI 集成
  • 功能齐全的 API(A full-featured API),脚本 API 提供多种模拟真实应用程序流量的功能。
    • 强大的 CLI 工具。
  • 内嵌的 JavaScript 引擎(An embedded JavaScript engine),提供了 Go 的性能,和 JavaScript 脚本的熟悉度灵活度
    • k6 嵌入了 JavaScript 运行时,可以使用 JavaScript ES2015/ES6 来编写脚本。
  • 多种协议的支持(Multiple Protocol support),HTTP,WebSockets 和 gRPC
  • 庞大的插件支持生态(Large extension ecosystem),借助 k6 来扩展需求,并且很多人通过社区分享他们的扩展
  • 灵活的指标存储和可视化(Flexible metrics storage and visualization),汇总统计数据或粒度指标,导出到您选择的服务。
  • 使用 Checks 和 Thresholds 可以更加轻松的做面向目标的自动化的负载测试。

为什么选择 k6

[[性能测试工具]]有很多

  • [[Hey]] Hey 是一个使用 Golang 实现的 HTTP 压测工具。
  • [[Apache Bench]] Apache Bench 是一个轻量的 HTTP 压力测试工具。
  • [[vegeta]] vegeta 是一个开源的 HTTP 压力测试工具。
  • [[Apache JMeter]],使用 Java 编写的开源的性能测试工具
  • [[Tsung]] Tsung 是一个 Erlang 编写的多协议支持的测试工具。
  • [[LOCUST]],LOCUST Python 编写的性能测试工具,基于代码的方式定义用户行为,支持分布式测试
  • [[Gatling]],Scala 编写,使用异步,非阻塞的方式模拟用户访问,提示实时的结果和报告
  • [[Siege]], Siege 是一款使用 C 语言编写的性能测试工具
  • [[Artillery]],Artillery 是一个 JavsScript 编写的,基于云端的压力测试工具。
  • [[Drill]],drill 是一个 [[Rust]] 编写的 HTTP 压力测试工具。

DLFh

k6 的优势在于 :

  • k6 支持 TypeScript 编写脚本,如果熟悉 TypeScript 学习成本比较低
  • 支持 metrics 输出,可以记录到 [[InfluxDB]] 等
  • 可以与多种 CI 工具集成,可以于 Grafana 进行集成展示结果数据

k6 vs JMeter

虽然 JMeter 在支持的协议数量上要优于 k6,但是 K6 相对于 JMeter 还是有不少的优势的 :

  • 因为 k6 是 Go 编写的,相对于 Java 编写的 JMeter 有性能上的差距,K6 可以只用较少的资源就能达到指定数量的负载。
  • 支持阈值
  • TypeScript 的脚本可以更好的促进协作和版本管理
  • 资源利用率远远强于 JMeter
  • 丰富的可视化方案

Load-testing

测试的类型

  • spike,尖峰测试,短时间内突然遭受到突发流量
  • stress,压力测试,将系统置于极限负载下
  • soak tests,持续性测试,连续时间内进行测试,通常是几小时或几天,这种测试旨在评估系统在长时间运行和持续负载下的稳定性,性能和资源管理能力

安装 k6

Debian/Ubuntu 可以执行如下命令1

sudo apt-get update && sudo apt-get install ca-certificates gnupg2 -y
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

Docker

docker pull loadimpact/k6

HTTP 请求

新建一个 script.js 文件

Get 请求 get(url, [params])

import http from 'k6/http';
// 默认控制选项
export let options = {
  vus: 10,         // 指定要同时运行的虚拟用户(VUs)数
  duration: '10s', // 指定测试运行的总持续时间
};
// default 默认函数
export default function() {
  // 标头
  let params = { headers: { 'Content-Type': 'application/json' } };
  var res=http.get("https://test.k6.io", params)
}

说明:

  • VUs, virtual users, 虚拟用户,更多的虚拟用户意味着更高的并发流量

对于 options 也可以通过设置可变的形式来设置不同阶段

export const options = {
  stages: [
    { duration: '30s', target: 20 },
    { duration: '1m30s', target: 10 },
    { duration: '20s', target: 0 },
  ],
};

Post 请求 Post( url, [body],[params])

import http from 'k6/http';
export let options = {
  vus: 100,
  duration: '10s',
};
// default 默认函数
export default function() {
  let json = { content: 'linhui', image: 'images' };
  let params = { headers: { 'Content-Type': 'application/json' } };
  var res = http.post("https://host/api/feedback", JSON.stringify(json), params)
  console.log(res.status);
}

del 请求 del(url,[body],[params])

import http from 'k6/http';
export let options = {
  vus: 1,
  duration: '10s',
};
// default 默认函数
export default function () {
  let json = {id:1};
  let params = { headers: { 'Content-Type': 'application/json' } };
  http.del('https://host/delete', json, params);
}

batch 批处理,可以用来做页面并发,批处理并不能保证执行顺序,batch(method,url,[body],[params])

import http from 'k6/http';
export let options = {
  vus: 1,
  duration: '10s',
};
export default function () {
  let get = {
    method: 'GET',
    url: 'https://host/get',
  };
  let get1 = {
    method: 'GET',
    url: 'https://host/get',
  };
  let post = {
    method: 'POST',
    url: 'https://host/post',
    body: {
      hello: 'world!',
    },
    params: {
      headers: { 'Content-Type': 'application/json' },
    },
  };
  let res = http.batch([req1, req2, req3]);
}

使用 request 发送求 request(method, url, [body], [params])

import http from 'k6/http';
export let options = {
  vus: 1,
  duration: '10s',
};
export default function () {
  let json = { content: 'linhui', image: 'images' };
  let params = { headers: { 'Content-Type': 'application/json' } };
  let res = http.request('POST', 'http://host/post', JSON.stringify(json), params);
  let res1 = http.request('GET', 'http://host/get', null, params);
}

执行脚本,进入脚本根目录

k6 run test.js
# 使用 docker
docker run -i loadimpact/k6 run - <test.js

常见指标说明

指标类型

名称描述
Counter计数器,对值进行累加
Gauge最小值、最大值和最后一个值。
Rate百分比
Trend最小值、最大值、平均值和百分位数的统计数据指标

k6 一直都会收集的指标

名称类型描述
vueGauge当前活动的虚拟用户数
vue_maxGauge虚拟用户的最大数量
iterationsCounter脚本中的函数被执行的次数
data_receivedCounter接收到的数据量大小
data_sentCounter发送的数据量大小
iteration_durationTrend完成默认/主函数的一次完整迭代所花费的时间。
checksRatechecks 项的成功率

HTTP 协议特有的指标

名称类型描述
http_reqsCounter总请求数量
http_req_blockedTrend在发起请求之前被阻塞的时间
http_req_connectingTrend建立到远程主机的TCP连接所花费的时间。
http_req_tls_handshakingTrend与远程主机握手建立TLS会话所花费的时间
http_req_sendingTrend将数据发送到远程主机所花费的时间
http_req_waitingTrend等待远程主机响应所花费的时间
http_req_receivingTrend从远程主机接收响应数据所花费的时间
http_req_durationTrend请求的总时间。它等于http_req_sending + http_req_waiting + http_req_receiving(即,远程服务器处理请求和响应花了多长时间,而没有初始DNS查找/连接时间)
http_req_failedRate失败请求率

每一个 http 都会返回一个 HTTP Response 对象,下面是常用的一些属性。

属性类型
Response.bodyHTTP 响应正文
Response.cookies响应 cookies ,属性是 cookie 名称,值是 cookie 对象数组
Response.error发送请求失败后的错误信息。
Response.error_code错误码
Response.headers标头,键值对
Response.status从服务器收到的 HTTP 响应代码
Response.timings耗时(以毫秒为单位)
Response.timings.blocked= http_req_blocked
Response.timings.connecting= http_req_connecting
Response.timings.tls_handshaking= http_req_tls_handshaking
Response.timings.sending= http_req_sending
Response.timings.waiting= http_req_waiting
Response.timings.receiving= http_req_receiving
Response.timings.duration= http_req_duration

自定义指标

import http from 'k6/http';
import { Trend } from 'k6/metrics';
export let options = {
  vus: 100,
  duration: '10s',
};
// 新建一个类型为 Trend 名为 sending_time 的自定制指标
let sendingTime = new Trend('sending_time');

export default function () {
  let res = http.get('http://www.baidu.com');
  sendingTime.add(res.timings.sending);
}

设置了 sending_time 这个自定义指标之后在 k6 运行的结果中就能看到新定义的指标。

常用 Option 选项

VUs:指定同时运行的虚拟用户数量,必须是一个整数,和 duration 搭配使用。默认值:1

export let options = {
  vus: 10,
  duration: '10s',
};

或者在执行命令时指定:

k6 run -u 10 test.js
k6 run --vus 10 test.js

Duration:字符串,指定测试运行的总持续时间,与 vus 选项一起使用。默认值:null

export let options = {
  vus: 10,
  duration: '10s',
};

执行命令时指定:

k6 run -u 10 --d 20s  test.js
k6 run --vus 10 --duration 20s  test.js

User Agent:发送 HTTP 请求时指定 User-Agent 标头。默认值:k6/0.27.0 (https://k6.io/) 取决于你 k6 的版本

export let options = {
  userAgent: 'Mozilla/5.0',
};
k6 run  --user-agent 'Mozilla/5.0'  test.js

TLS Version:表示允许在与服务器交互中使用的唯一 SSL/TLS 版本的字符串,或者一个指定允许使用的“最小”和“最大”版本的对象。 默认值:null (允许所有版本)

export let options = {
  tlsVersion: 'tls1.2',
};

export let options = {
  tlsVersion: {
    min: 'ssl3.0',
    max: 'tls1.2',
  },
};

TLS Cipher Suites:允许在与服务器的 SSL/TLS 交互中使用的密码套件列表。由于底层 go 实现的限制,不支持更改 TLS 1.3 的密码,并且不会执行任何操作。 默认值:null(允许所有)

export let options = {
  tlsCipherSuites: [
    'TLS_RSA_WITH_RC4_128_SHA',
    'TLS_RSA_WITH_AES_128_GCM_SHA256',
  ],
};

TLS Auth: tls 身份验证。默认值:null

export let options = {
  tlsAuth: [
    {
      domains: ['example.com'],
      cert: open('mycert.pem'),
      key: open('mycert-key.pem'),
    },
  ],
};

Throw:一个布尔值,true or false ,指定是否在失败的 HTTP 请求上抛出异常。 默认值:false

export let options = {
  throw: true,
};
k6 run  --throw test.js
k6 run  -w test.js

Thresholds:一组阈值规范,用于根据指标数据配置在何种条件下测试成功与否,测试通过或失败。默认值:null

export let options = {
  thresholds: {
    http_req_duration: ['avg<100', 'p(95)<200'],
    'http_req_connecting{cdnAsset:true}': ['p(95)<100'],
  },
};

Tags:指定应在所有指标中设置为测试范围的标签。如果在请求、检查或自定义指标上指定了同名标签,它将优先于测试范围的标签。 默认值:null

export let options = {
  tags: {
    name: 'value',
  },
};
k6 run --tag NAME=VALUE test.js

RPS:指的是每秒发出的最大请求数。 默认值:0

export let options = {
  rps: 500,
};

或者:

k6 run --rps 500  test.js

Paused:是否可以暂停和和恢复的方式运行脚本,暂停启动后需要使用另外的窗口执行k6 resume 恢复使用。在恢复窗口可以实时的查看脚本的运行情况。 启动后不支持暂停, 默认值:false

export let options = {
  paused: true,
};
k6 run --paused  test.js
k6 run --p test.js

No VU Connection Reuse:布尔值,是否复用 TCP 链接。默认值:false

export let options = {
  noVUConnectionReuse: true,
};
run --no-vu-connection-reuse  test.js

No Usage Report:布尔值,是否给 k6 发送使用报告,true 值不会发使用报告。 默认值:false

k6 run --no-usage-report test.js

No Thresholds:布尔值,是否禁用阈值。默认是:fasle

k6 run --no-thresholds test.js

No Summary:是否禁用测试结束生成的概要。默认值:false

k6 run --no-summary test.js

No Cookies Reset:是否重置 Cookies,fasle 每次迭代都会重置 Cookie ,true 会在迭代中持久化 Cookie 。默认值:false

export let options = {
  noCookiesReset: true,
};

No Connection Reuse:是否禁用保持活动连接,默认值:false

export let options = {
  noConnectionReuse: true,
};
k6 run --no-connection-reuse test.js

Minimum Iteration Duration:指定默认函数每次执行的最短持续时间,任何小于此值的迭代都将剩余时间内休眠,直到达到指定的最小持续时间。默认值:0

export let options = {
  minIterationDuration: '10s',
};
k6 run --min-iteration-duration '1s' test.js

Max Redirects:最大重定向,默认值:10

export let options = {
  maxRedirects: 10,
};
k6 run -max-redirects 10 test.js

Batch: batch 同时调用的最大连接总数,如果同时有 20 api 请求需要发出 ,batch 值是 15,那么将会立即发出 15 个请求,其余的请求会进行一个排队。默认值:20

export let options = {
  batch: 15,
};

k6 run --batch 10 test.js

Batch per host:batch 对同一个主机名同时进行的最大并行连接数。默认值:6

export let options = {
  batchPerHost: 5,
};
k6 run --batch-per-host 10 test.js

Blacklist IPs:黑名单。默认值:null

export let options = {
  blacklistIPs: ['10.0.0.0/8'],
};
k6 run --blacklist-ip= ['10.0.0.0/8'] test.js

Block Hostnames:基于模式匹配字符串来阻止主机,如 *.example.com , 默认值:null

export let options = {
  blockHostnames: ["test.k6.io" , "*.example.com"],
};
k6 run --block-hostnames="test.K6.io,*.example.com" test.js

Discard Response Bodies:是否应丢弃响应正文,将 responseType 的默认值修改成 none,建议设置成 true,可以减少内存暂用和 GC 使用,有效的较少测试机的负载。默认值:false

export let options = {
  discardResponseBodies: true,
};

HTTP Debug:记录所有 HTTP 请求和响应。默认情况下排除正文,包括正文使用 –http debug=full 默认值:false

export let options = {
  httpDebug: 'full',
};
k6 run --http-debug test.js

Options 的顺序

k6 提供了很多方式来设置 Options.

  • CLI 选项
  • 环境变量
  • 脚本中的 options 对象

执行的顺序,优先级从低到高:

  • Defaults,默认使用
  • --config
  • Script options
  • Environment variables
  • CLI Flags

如果 Options 发生冲突,那么 k6 会遵循上面的优先级,CLI flags 中的是最高优先级,会覆盖其他所有设置。

Checks 检查

Checks 类似断言,不同在于 Checks 不会停止当前的脚本。指标都可以作为检查的项目。

import http from 'k6/http';
import { sleep } from 'k6';
import { check } from 'k6';

export let options = {
  vus: 100,
  duration: '10s',
};
export default function () {
  let res = http.get('http://test.k6.io/');
  check(res, {
    '状态码为200': (r) => r.status === 200,
    '响应时间小于200ms': (r) => r.timings.duration < 200,
    '等待远程主机响应时间小于200ms': (r) => r.timings.waiting < 200,
  });
}

Thresholds 阈值

阈值是用来指定被测系统的性能预期的通过/失败标准。阈值用来分析性能指标并确定最终测试结果。内置的指标都可以作为阈值。

k6 中包含的四种度量类型每一种都提供了自己的一组可用于阈值表达式的聚合方法。

  • Counter: count and rate
  • Gauge:value
  • Rate:rate
  • Trend:p(N)
import http from 'k6/http';
import { Trend, Rate, Counter, Gauge } from 'k6/metrics';
export let GaugeContentSize = new Gauge('ContentSize');
export let TrendRTT = new Trend('RTT');
export let options = {
  vus: 10,
  duration: '10s',
  thresholds: {
    // 发出的请求数量需要大于1000
    http_reqs:['count>1000'],
    // 错误率应该效率 0.01%
    http_req_failed: ['rate<0.01'],
    // 返回的内容必须小于 4000 字节。
    ContentSize: ['value<4000'],
    // p(N) 其中 N 是一个介于 0.0 和 100.0 之间的数字,表示要查看的百分位值,例如p(99.99) 表示第 99.99 个百分位数。这些值的单位是毫秒。
    // 90% 的请求必须在 400 毫秒内完成,95% 必须在 800 毫秒内完成,99.9% 必须在 2 秒内完成
    http_req_duration: ['p(90) < 400', 'p(95) < 800', 'p(99.9) < 2000'],
    // 99% 响应时间必须低于 300 毫秒,70% 响应时间必须低于 250 毫秒,
    // 平均响应时间必须低于 200 毫秒,中位响应时间必须低于 150 毫秒,最小响应时间必须低于 100 毫秒
    RTT: ['p(99)<300', 'p(70)<250', 'avg<200', 'med<150', 'min<100'],
  },
};

export default function () {

  let res = http.get('http://www.baidu.com');
  TrendRTT.add(res.timings.duration);
  GaugeContentSize.add(res.body.length);
}

阈值标签,测试中可以给指定的 url 或者特定标签上使用阈值。

import http from 'k6/http';
import { sleep } from 'k6';
import { Rate } from 'k6/metrics';

export let options = {
  vus: 10,
  duration: '10s',
  thresholds: {
    // type 为 baidu 使用
    'http_req_duration{type:baidu}': ['p(95)<500'],
    // type 为 bing 使用
    'http_req_duration{type:bing}': ['p(95)<200'],
  },
};

export default function () {
  let res1 = http.get('https://www.baidu.com', {
    tags: { type: 'baidu' },
  });
  let res2 = http.get('https://cn.bing.com/', {
    tags: { type: 'bing' },
  });

  let res3 = http.batch([
    [
      'GET',
      'https://www.baidu,com',
      null,
      { tags: { type: 'baidu' } },
    ],
    [
      'GET',
      'https://cn.bing.com/',
      null,
      { tags: { type: 'bing' } },
    ],
  ]);

}

默认情况下没有达标阈值标准是不会停止脚本的,通过设置阈值的 abortOnFail: true 来终止。

import http from 'k6/http';
export let options = {
  vus: 10,
  duration: '10s',
  thresholds: {
    http_req_duration: [{threshold: 'p(99) < 10', abortOnFail: true}],
  },
};

export default function () {
  let res = http.get('http://www.baidu.com');
}

对通过的阈值前面会有一个 ✓,而失败的则会有一个 ✗ 。只有满足所有阈值的情况下测试才算通过。

日志输出

输出到控制台。

import http from 'k6/http';
export let options = {
  vus: 10,
  duration: '2s',
};

export default function () {
  let res = http.get('http://www.baidu.com');
   console.log('log')
   console.info('info');
   console.error('err');
   console.debug('debug')
   console.warn('warn')
}

输出到文件,输出到文件的同时控制台不在输出。

k6 run  test.js --console-output=test.log

InfluxDB Grafana 可视化测试结果

Docker 启动 [[InfluxDB]]

docker pull tutum/influxdb
# 8083是 influxdb 的 web 管理工具端口,8086 是 influxdb 的 HTTP API 端口
docker run -d -p 8083:8083 -p8086:8086 --expose 8090 --expose 8099 --name influxsrv tutum/influxdb

Docker 启动 Grafana,

docker pull grafana/grafana
docker run -d -p 3000:3000 grafana/grafana

新建一个 k6test 数据库,访问 “http://xxxxx:8083” InfluxDB web 管理页面,新建一个 K6test 数据库

DaYC

配置 Grafana 数据源

DiNH

选择 InfluxDB, 填写域名端口和数据库,点击 sava&test 。出现 Data source is working 表示成功,如遇到问题查看一下端口是否放行。

导入仪表盘

DCTD

通过 ID 导入,输入 2587 点击 load 数据源选择 InfluxDB 点击 Import

官方还有几款仪表盘

将 k6 的测试指标导入到 InfluxDB

k6 run --out influxdb=http://xxxxx:8086/K6test test.js

效果图

DEgL

Scenarios

Scenarios (场景)配置 VU 和迭代计划的方式,通过 Scenarios 配置,可以在负载测试中对不同的工作负载或流量模式进行建模。

使用场景的优势 :

  • 同一个脚本中可以声明多个场景,每个场景都可以执行不同的 JavaScript 函数
  • 模拟更真实的流量,每个场景都可以使用不同的 VU 和调度模式
  • 并行或顺序执行测试
  • 每个场景都可以设置不同的环境变量和指标
export const options = {
  scenarios: {
    example_scenario: {
      // name of the executor to use
      executor: 'shared-iterations',

      // common scenario configuration
      startTime: '10s',
      gracefulStop: '5s',
      env: { EXAMPLEVAR: 'testing' },
      tags: { example_tag: 'testing' },

      // executor-specific configuration
      vus: 10,
      iterations: 200,
      maxDuration: '10s',
    },
    another_scenario: {
      /*...*/
    },
  },
};

executor 场景必须使用预定义的 executor 来配置,设置运行的时间,流量是否保持恒定,工作负载是按 VU 还是按到达率。

  • 按迭代次数
    • shared-iterations 在 VUs 之前 shares iterations,在所有并发的 VUs 中共享迭代,当所有的迭代完成时,测试结束
    • per-vu-iterations 让每一个 VU 运行配置的迭代,每个 VU 执行一定数量的迭代。
  • 按 VU 数量
    • constant-VUs 恒定数量发送 VU,在指定的时间内以恒定的并发虚拟用户数 (VUs) 执行
    • ramping-vus 根据配置阶段性增加 VU 数量,缓慢增加并发用户数
  • 按 iteration rate
    • constant-arrival-rate 以恒定速率开始迭代,
    • ramping-arrival-rate 根据配置的阶段提高 iteration rate

Scenario example

import http from "k6/http";

export const options = {
  scenarios: {
    shared_iter_scenario: {
      executor: "shared-iterations",
      vus: 10,
      iterations: 100,
      startTime: "0s",
    },
    per_vu_scenario: {
      executor: "per-vu-iterations",
      vus: 10,
      iterations: 10,
      startTime: "10s",
    },
  },
};

export default function () {
  http.get("https://test.k6.io/");
}
  • shared_iter_scenario 10 个 VU 进行 100 次迭代
  • per-vu-iterations 在 10 秒之后开始,10 个 VU 每个运行 10 次迭代

总结

k6 是一个非常强大的性能测试工具,只需要稍微了解一下 TypeScript,熟悉一下调用过程,就可以很快上手使用。

reference


2023-07-01 k6 , grafana , load-testing , typescript , open-source , go-lang , jmeter

修复 Ledger Nano X 转轴松垮问题

之前有写过一篇文章说我买了一个Ledger Nano S Plus ,整体来看 Nano S Plus 没有蓝牙功能,机身塑料质感非常强,后来看到 Nano X 打折,价格基本上已经快接近 Nano S Plus 的价格了,所以就又下单了一个 Nano X。但是没想到收货之后,发现其塑料质感和做工更加不行,并且我到货的 X,转轴部分非常松垮,只要稍微侧着拿起外面的部分,中间的机身就会溜出来。

DIs8

后来简单的查了一下之后才发现原来这是 Nano X 的通病,很多人都遇到了和我一样的问题,没想到的是 100 多美元的东西做工质量这么差。但是好在这个问题可以手动进行修复。

查了之后才发现原来 Nano X 的机身和外部的铁质的外壳是可以分离的,不像 Nano S Plus 的那样是一体的。将 Nano X 的外壳铁质部分分离之后,可以将铁质部分用手或者工具稍微向内弯曲一下。力度不要太大,只需要有一个合理的弯度,可以正好卡住机身的转轴即可。

当弯曲合适之后,再将机身放入到外壳部分卡住。测试一下摩擦是否合适,如果还是太松,就重复上面的动作,直到一个合适的贴合度。


2023-06-30 ledger , ledger-nano-x , cryptocurrency , hardware-wallet , cryptocurrency-wallet

Tana 使用体验

Tana 又是一款 All-in-one 的笔记软件。我虽然很早就已经拿到了 Tana 的使用体验,但是初次体验之后因为其只提供了在线版本,无法离线使用,很快就放弃了。还是坚守在 [[Obsidian]] ,在桌面版上,随着插件的不断丰富已经成为了我常驻应用了,代替了我之前的所有笔记软件,代替了 Read it later 应用,代替了我的 WordPress 编辑器,代替了我 Jekyll 的编辑器,还代替了我用了很久的 Trello,并且我发现本地的看板完全可以代替需要网络 Loading 才能使用的 Trello。

但这一次再次想起了 Tana 是因为我又看到有人提起了 Supertags 的概念,这是一个陌生的概念。

Tana 的特性

  • 没有文件夹概念
  • Supertags
  • 搜索 (Live query)
  • 多人协作
  • 唯一链接(用于分享)

消除了笔记的边界

在初次体验 Tana 的时候,最初的印象就是 Tana 的设计,所有的笔记都是通过当天的笔记开始,每一个笔记都是一个「节点」。节点之间可以相互引用,嵌套。

在 Tana 中已经完全没有了传统笔记的文件夹,文件(文档)的概念,最顶层的组织结构 Tana 称之为 Workspaces,在 Workspaces 下所有的笔记都只有节点的概念,它们可以并列,嵌套,以任意的组织形式存在。

AQU0

什么是 Supertags

对于普通的 Tag,我们都非常熟悉,不论是文章,还是帖子,还是 Twitter,我们都可以用 #tag 这样的方式来对文章内容进行贴标签。通过标签我们可以很快的定位这个文章、这个帖子是什么样的内容,而搜索引擎或者推荐系统也可以很快速的识别这个内容的分类。同样的浏览者也可以通过标签来找到更多的相似的内容。

而 Tana 定义的 Supertags 就是给普通的标签(tag)加上了元数据(metadata)。在 [[Obsidian]] 中普通笔记最基本的描述信息就是标签,Obsidian 也提供了方法让笔记通过标签的形式组织和检索,但是 Obsidian 中 Tag 就是最小的单位了。

为什么需要 Supertags

相信说到上面,很多就有疑问了,既然已经有了传统的 Tag,为什么还需要一个新概念 Supertags? 在上文中我提到在 Tana 中已经模糊了笔记的边界,所有节点都是一条笔记,笔记和笔记之前没有了明显的边界,那么如何在一个庞大的节点库中对这些内容进行组织就成为了棘手的问题,通过搜索?显然不够迅速,通过传统的标签,最后就会像 Obsidian 中那样还是退化成为了标签的搜索。

而 Tana 提出的解决办法就是 Supertags,比如官方的例子中,用户可以对不同节点中的内容打上 #book 的标签,并且结合 #book 和内容本身,对 book 标签进行进一步的扩充,比如 book 可以包含作者,出版时间,包含简介等等更具体的信息。在 Tana 中官方也实现了一个 AI 自动进行标签填充的功能(借助 OpenAI)。

有了这个功能之后,比如我经常在 Obsidian 中记录读书笔记的时候,常常在以书名命名的笔记前使用 YAML 定义笔记的 metadata(借助模板),而在 Tana 中我仅仅需要在节点上添加一个 #book 标签,那这个节点所需要的所有元数据都会包含在这个标签中。通过这样组织方式,节点之间会形成非常复杂的关系。

而每当用户创建一个标签,Tana 都会自动提示创建一个 List of #book 通过标签的关键,可以直接查询出所有包含这个 #book 的笔记内容,间接地将网状结构的笔记内容提取成为了一个线性的。怪不得 Tana 自己在介绍 Supertags 的时候就称 Supertags 是所有内容的 Glue。

Supertags 的继承关系

在 Tana 之中,标签是有继承关系的,我们都知道面向对象编程三大特点之一的继承目的就是为了复用代码,而在 Tana 中,通过继承(Extend)就使得标签也可以继承标签。这样就非常容易扩展标签。

比如有 #book 这样的标签,同样会有 #fiction#non-fiction 等等的标签,而这些具体的标签都可以继承 #book 的属性。

Supertags 能做什么

一个设计良好的 Supertags 可以让界面变成任何你想要达成的效果。

比如设计一个 #todo 标签,包含 Start Date, Due Date,Status 等属性,那么就可以轻松地创建一个类似 Trello 的看板画面。

如果像上面一样创建一个 #book 权限,添加一个属性 Status(待读,在读,已读),你就创建了一个类似豆瓣的 Track 读书记录的工具。

AtF6

所以这就是为什么很多人说 Tana 实际上就是一个数据库的原因。只要用户发挥想想,充分利用 Tana 的展示形式(List,Table,Cards,Tabs,Calendar),以及 Filter,Group,Sort 就能展示出任何想要的界面。

获取 Tana 的测试权限

  • 官网 申请进入 waitlist
  • 进入官方的 Slack 在频道中自我介绍获取
  • 找拥有 Tana 邀请权限的用户
    • 可以加入 Telegram Group 找我索要,但是我只有一个邀请,先到先得,加入群组的朋友们可以相互帮助一下

总结

只有当一个工具对当前工作效率提高具有压倒性的优势时,我才会考虑替换。我过去曾经使用很多年的 [[WizNote]],积累了上千条笔记,虽然有 Evernote,OneNote 等等工具的出现,但都没有让我切换过去,直到 Obisidian 的出现,弥补了 WizNote 对 Markdown 的支持,强大的双链,非常快速的检索,以及强大的社区插件支持,让我在体验不久之后就毅然决定使用,并一直使用到现在。

我切换使用 Obsidian 的决定性原因就是 Obsidian 让我意识到了笔记不是一个摘录的工具,而是可以组织自己想法,让想法和想法之间产生关联的思考方式。并且 Obisidian 的全离线化设计让我非常放心的不会去考虑隐私等等问题。Obsidian 的开放程度也让 Obsidian 可以快速的融入任何其他的工具中,[[Anki]],[[Zotero]],[[Readwise]] 等等。

Tana 的设计思想,确实融合了很多优秀笔记的特点,但是其 Supertags 的功能,还不足以让我放弃现在的 Obsidian。所以之后还会继续观察 Tana 的发展。

不足

  • 编辑器的样式不足,我之前总结的 Vim Everywhere 在这里就失效了,纯网页的操作,缺少了 Vim 快捷键的操作逊色了不少
  • 不支持 Markdown
  • 数据存储在云端,只支持手动导出 JSON 格式的文档

reference


2023-06-25 note-taking , note , obsidian , pkm , supertag , knowledge-graph , personal-knowledge-management

macOS 下利用 Karabiner Elements 修改日本 JIS 键盘布局到美式键盘布局

JIS 键盘布局

JIS 键盘布局是日本是最常见的键盘布局,它与国内常用的美式键盘布局有一些不同之处。如果你习惯了美式键盘布局,使用 JIS 键盘可能会感到不便。

可以明显感到区别的是

  • JIS 键盘的空格键非常短,我个人几乎只能使用左手大拇指来按
  • 空格左边原来非常高频使用的 Command 按键变成了「英数」按键,同样空格键右侧的 Command 按键也变成了 かな
  • 原来美式键盘的 Caps Locks 变成了 control 按键,虽然使用 Vim 有些人还会被鸡肋的 Caps Locks 手动修改成 Control 键,当时我一直都是将 Caps Locks 修改成按下就表示同时按下 Command + Ctrl + Option + Shift 这样的组合按键(Hyper Key),然后通过 Caps Locks 按键组合其他快捷键就可以实现非常多的功能,比如 Caps Locks + hjkl 快速调节窗口的位置,Caps Locks + f 全屏窗口等等,我都用 [[Hammerspoon]] 和 [[Karabiner Elements]] 这样的工具实现了,但是在 JIS 键盘下就把我的习惯给打破了
  • 最后一个令我非常不习惯的就是数字按键 1 左边的 ~ 按键,写 Markdown 非常常用的 tilde 按键(行内格式化代码) 在 JIS 键盘中消失了
  • 最后还有一些细节部分,比如 JIS 键盘上退格键非常小!但是 Enter 键却又特别大!

A8NI

通过 Karabiner Elements 修改键盘布局

Karabiner Elements 是一款强大的键盘映射工具,可以很容易地修改 JIS 键盘布局到美式键盘布局。本文将向你展示如何使用 Karabiner Elements 来实现这一目标。

首先,你需要下载并安装 Karabiner Elements。你可以在官方网站上找到适用于 macOS 的安装程序,并按照指示进行安装。一旦安装完成,你就可以打开 Karabiner Elements 并开始配置键盘映射。

或者通过 brew 来安装

brew install --cask karabiner-elements

在 Karabiner Elements 的界面中,点击左侧的“Complex Modifications”选项卡,然后点击右下角的“Add rule”按钮。在这个界面也可以通过从在线网站导入的方式。点击 Import more rules from Internet,来从网站下载更多的规则。打开网站规则列表,其中包含各种可用的键盘映射规则。

在规则列表中,找到“Japanese JIS Keyboard to US Keyboard:Remap Symbol Keys”规则,并勾选它。这将启用该规则并应用键盘映射。

接下来,你可以点击右侧的“Details”按钮,以查看该规则的详细信息。在这里,你可以看到该规则将会对哪些键进行映射,以及映射的具体内容。默认情况下,该规则将会将 JIS 键盘布局中的一些键映射为美式键盘布局中的相应键。例如,JIS 键盘布局中的 “@” 键将会映射为 “[” 键,而 “[” 键将会映射为 “]” 键。

现在,可以关闭 Karabiner Elements,并开始使用修改后的键盘布局。打开任何文本编辑器或其他应用程序,你会发现 JIS 键盘布局已经被修改为美式键盘布局。


2023-06-20 japanese-keyboard , japanese , mac , keyboard , keyboard-layout , us-layout

推荐几个英语学习工具

要想学好一门语言,是一个需要长期花时间的过程,听说读写一样都不能落下。之前我也要推荐过学习英语,就应该使用英英字典(当达到一定的词汇量之后),学习英语就应该让自己的耳朵沉浸在英语发音中(可以通过听英语播客),当能够听懂大部分的内容的时候,无字幕看电影也是一种沉浸式的学习方法,到通过[[影子跟读法]] 来练习发音,完成一个语言的听说读写之后才能形成一个学习的闭环。

这篇文章抛开那些枯燥的学习方法介绍几个非常有意思的语言学习小工具。

  • [[Language Reactor]] 是一个在视频上实时显示字幕的 Chrome 插件
  • [[YouGlish]] 是一个可以通过关键字来搜索发音的网站,非常适合用来学习单词或短语的发音
  • [[Immersive Translate]] 是一款沉浸式的翻译插件,在浏览网页时可以将翻译内容附在原文下方。
  • [[selectext]] 是一款可以复制视频内容的 Chrome 插件

Language Reactor

Language Reactor 是一个 Chrome 插件,可以在 Netflix,YouTube 等等流媒体平台上实时显示多语言字幕。

AXnH

YouGlish

YouGlish 是一个通过单词短语来搜索视频(音频)的网站,非常适合用来学习英语发音,在网站中随便搜索一个单词,YouGlish 就会在 Youtube 上搜索包含这个单词的视频句子,听到真正的 Native Speaker 发音,可以通过模仿,重复来提高自己的英语口语。

也可以选择各种类型的发音,当然也可以学习其他语言。

A9Dr

这个网站支持非常多的语言。

AAJh

比如日语:

AU6C

Immersive Translate

Immersive Translate 是一款开源的 Chrome 插件,可以将网页的内容翻译在原文下方。

比如当看到日文的,或英文的网页时,可以将翻译内容直接显示在文字下方。

AfKD

selectext

selectext 是一款可以复制视频内文字的浏览器插件。当你在看英文,或其他语言教程的时候,遇到想要查字典的单词,可以暂停视频,然后复制视频中的文字。

AuGL

比如当看到这个视频,想要查一下 町 的意思,就可以暂停视频,然后点击视频左上方的 S,之后插件就会对视频内容进行 OCR,视频出其中的文字,然后点击其中的方框就立即能复制到粘贴板中。


2023-06-17 english , language-learning , language

XML 数字签名及 Java 实现

数字签名是一种基于摘要算法和非对称加密技术的防止数据在传输过程中被篡改的安全技术。

数字签名的原理是对传输的内容做摘要(SHA245 等),然后把摘要和用到的摘要算法使用非对称加密技术的公钥或私钥(大部分情况下是私钥)生成签名。接收方接受到数据后,把签名信息用私钥或公钥验证来确保内容的完整性。

XML 数字签名

XML 数字签名是数字签名的基础上定义出来的一种 XML 数字签名规范,和普通的数字签名相比较有不少优点,比较灵活。XML 数字签名即可以对传输的所有内容签名,也可以只对传输的一小部分内容进行部分签名。不同的签名还可以使用不同的算法和密钥。[^1]

[^1]:https://www.w3.org/TR/xmldsig-core2/

XML Signature 是一个定义数字签名的 XML 语法的 W3C 推荐标准。从功能上或,XML Signature 与 PKCS#7 有很多共同点,但是 XML 签名具有更好的可扩展性,并为签名 XML 文档做了调整。XML Signature 在许多 Web 技术,如 SOAP, SAML 等中使用。

最近在调研日本 e-Gov 电子申请的时候,所使用的签名技术就是基于 XML 的签名,因为提交的数据格式是 XML ,并且涉及到敏感信息所以签名是必不可少的部分。

XML 数字签名的类型

XML 数字签名分成三种类型:

  • Enveloped
  • Enveloping
  • Detached

它们的差别在于 XML 文档结构不同。

Enveloped

Enveloped 格式 XML 签名是把签名节点 Signature 直接嵌入到原始 XML 文档中。比如下方的样式中的 <Signature> 节点。

e-Gov 采用的方式就是这个 Enveloped 格式。

<?xml version="1.0" encoding="UTF-8"?>
<POrder>
 <Item number="130055555232">
  <Description>Game</Description>
  <Price>19.99</Price>
 </Item>
 <Customer id="8492340">
  <Name>My Name</Name>
  <Address>
   <Street>One Network Drive</Street>
   <Town>Burlington</Town>
   <State>MA</State>
   <Country>United States</Country>
   <PostalCode>01803</PostalCode>
  </Address>
 </Customer>
 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  <SignedInfo>
   <CanonicalizationMethod
    Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
   <SignatureMethod
    Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
   <Reference URI="">
    <Transforms>
     <Transform
      Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
    </Transforms>
    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
    <DigestValue>tVicG91o5+L31M=</DigestValue>
   </Reference>
  </SignedInfo>
  <SignatureValue>
   ...
  </SignatureValue>
  <KeyInfo>
   <X509Data>
    <X509SubjectName>
     CN=Your Name,O=Certificates Inc.,C=US
    </X509SubjectName>
    <X509Certificate>
     ...
    </X509Certificate>
   </X509Data>
  </KeyInfo>
 </Signature>
</POrder>

Enveloping

Enveloping 格式的 XML 签名和 Enveloped 正好相反,把原始 XML 文档作为子节点,插入到新生成的 Signature 节点的 Object 子节点中。

<?xml version="1.0" encoding="UTF-8"?>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  <SignedInfo>
   <CanonicalizationMethod
    Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
   <SignatureMethod
    Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
   <Reference URI="#order">
    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
    <DigestValue>+8cIU+LQ=</DigestValue>
   </Reference>
  </SignedInfo>
  <SignatureValue>
   ...
  </SignatureValue>
  <KeyInfo>
   <X509Data>
    <X509SubjectName>
     CN=Your Name,O=Test Certificates Inc.,C=US
    </X509SubjectName>
    <X509Certificate>
     ...
    </X509Certificate>
   </X509Data>
  </KeyInfo>
  <Object ID="order">
      <POrder>
       <Item number="130045555532">
        <Description>Game</Description>
        <Price>19.99</Price>
       </Item>
       <Customer id="849">
        <Name>Your Name</Name>
        <Address>
         <Street>One Network Drive</Street>
         <Town>Burlington</Town>
         <State>MA</State>
         <Country>United States</Country>
         <PostalCode>01803</PostalCode>
        </Address>
       </Customer>
      </POrder>
   </Object>
 </Signature>

这里需要注意的是 Reference 节点中指向的部分,如果使用空值表示指向文档根节点,而如果指定了值,那就是指向 XML 文档中的部分内容。

Detached

Detached 格式是指新生成的 Signature 节点作为一个独立的文档单独保存和传输,而不会对原始文档进行修改。

数字签名的应用场景

  • 可靠信息交换
  • 电子公文传输

XML 数字签名的结构

Signature 结构

对于 XML 签名来说就是根据 XML 文档内容以及证书生成下面的一个签名结构。

<Signature ID?>
  <SignedInfo>
    <CanonicalizationMethod />
    <SignatureMethod />
   (<Reference URI? >
     (<Transforms>)?
      <DigestMethod>
      <DigestValue>
    </Reference>)+
  </SignedInfo>
  <SignatureValue>
 (<KeyInfo>)?
 (<Object ID?>)*
</Signature>

举例

<署名情報>
<Signature
	xmlns="http://www.w3.org/2000/09/xmldsig#" Id="20230720113000">
	<SignedInfo>
		<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>
		<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></SignatureMethod>
		<Reference URI="#%E6%A7%8B%E6%88%90%E6%83%85%E5%A0%B1">
			<Transforms>
				<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></Transform>
			</Transforms>
			<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>
			<DigestValue>BdQwkfm3lyDWV2mTu+CxBPU=</DigestValue>
		</Reference>
		<Reference URI="900A01000200800001_01.xml">
			<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>
			<DigestValue>2WrlTW71oH+E6FuhxGR0=</DigestValue>
		</Reference>
	</SignedInfo>
	<SignatureValue>gjeaEM7qng==</SignatureValue>
	<KeyInfo>
		<X509Data>
			<X509Certificate>MIIEizCU</X509Certificate>
		</X509Data>
	</KeyInfo>
</Signature>
</署名情報>
  • Signature 节点是数字签名根节点
  • SignedInfo 保存签名和摘要信息以及使用的各种算法
    • SignedInfo 中的 CanonicalizationMethod 子节点用来指定生成签名的 SignedInfo 节点规范化处理方法,具体的方法可以参考规范中的 「Exclusive XML Canonicalization」
      • 只有对 XML 内容规范化之后,才不会因为 XML 文档格式稍有不同而影响验证结果
    • SignatureMethod 子节点用来指定签名使用的摘要算法签名算法
      • 如果 Algorithm 值是 http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 表示使用的签名算法是 SHA256-RSA
    • SignedInfo 可以包含一个或多个 Reference 子节点,每个 Reference 用来指定某个引用的 XML 节点经过规范化后的摘要信息和生成摘要的方法
      • Reference 中会包含一个 URI 属性,用来指定对 XML 中哪一个节点进行签名,一般会是 #node,或者直接是指定文件名。
      • Reference 中同样包含两个子节点,分别表示摘要的算法以及对指定内容进行摘要之后的值
  • SignatureValue 用来记录整个 SignedInfo 节点经过规范化后输出内容的签名,并使用 Base64 编码算法转换成可见的字符串
  • KeyInfo 可选,用来保存验证签名的非对称加密算法公钥(只有公钥可以公开)
  • Object 节点是可选的,只有在 Enveloping XML 签名时才会用到

开发相关的库

  • [[Apache Santuario]] 是 Apache 上一个 XML 安全性方面的项目,旨在实现对 XML 的主要安全标准。

XMLSec Library

XMLSec Library 支持 W3C 的 XML Signature 和 XML Encryption 规范,同时也支持 Canonical XML 和 Exclusive Canonical XML 规范。

XML 数字签名处理过程

主要工作是根据内容创建 Signature 节点。主要是分成三个步骤

  • 第一步是根据指定的 XML 节点,生成 SignedInfo 中的 “Reference” 节点,Reference 节点会指定需要签名的对象,然后根据 Reference 指定的签名算法,生成签名值,比如在电子申请中,有两个 Reference,一个是 構成情報,另外一个是手续的申请书。
  • 第二步是在 Reference 的基础上创建 SignedInfo 节点,并指定 SignedInfo 需要的规范化处理方法,以及签名算法
  • 最后是根据 SignedInfo 以及证书,生成数字签名 SignatureValue,并最终生成完整的 Signature 节点

当有了完整的 Signature 节点之后再根据不同的格式对 XML 内容进行操作。

XML 签名验证

有 XML 签名的过程,同样在验证时只需要对上面的过程进行逆向就可以。当接收方接收到了包含 Signature 的 XML 文档。

  • 首先根据 SignedInfo 中指定的规范化方法处理整个 SignedInfo 节点,保证不会因为格式问题出错,然后根据 SignatureMethod 中的数字签名算法验证签名信息,如果验证通过表示 SignedInfo 中的内容没有被篡改
  • 然后开始验证 SignedInfo 中所有的 Reference 节点
    • 通过定义的 URI 找到对应的 XML 节点,通常这个 XML 的 ID 应该是整个文档唯一的
    • 根据 XML 中的节点,按照 Reference 中的 Transforms 指定的规范化方法处理
    • 然后根据 DigestMethod 中指定的摘要算法对规范化之后的内容进行摘要处理
    • 然后将摘要信息和 DigestValue 中的摘要值(Base64 解码)进行对比,一致则表示通过

通过以上的验证就可以确保传输的内容没有被篡改。

Java 实现

引入依赖

<dependency>
  <groupId>org.apache.santuario</groupId>
  <artifactId>xmlsec</artifactId>
  <version>2.2.3</version>
</dependency>

编码例子

public static void sign(Key signKey, X509Certificate signCert, Element signElement) throws XKMSException {
  String elementId = signElement.getAttribute("Id");
  if (elementId == null) {
    throw new XKMSException("Id of the signing element is not set");
  }
  String elementRefId = "#" + elementId;
  IdResolver.registerElementById(signElement, elementId);
  try {
    XMLSignature signature = new XMLSignature(signElement
        .getOwnerDocument(), elementRefId,
        XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1,
        Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
    signElement.appendChild(signature.getElement());
    Transforms transforms = new Transforms(signElement
        .getOwnerDocument());
    transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
    transforms
        .addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
    signature.addDocument(elementRefId, transforms,
        MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA1);
    signature.addKeyInfo(signCert);
    signature.addKeyInfo(signCert.getPublicKey());
    signature.sign(signKey);
  } catch (XMLSecurityException xmse) {
    throw new XKMSException(xmse);
  }
}

reference


2023-06-11 java , xml , java-xml , java-xml-signature , signature

macOS 上的清理工具整理合集

最近 macOS 系统磁盘空间告急,之前就出现过因为磁盘空间不足导致系统卡顿还出现突然黑屏的状态,所以这次就看到还剩余几十个 GB 的时候就开始清理工作了。清理的同时顺便就整理一下常用的几个清理工具。

如何发现大文件

在清理之前首先要对本地磁盘文件做一个整体的了解,虽然 macOS 自带一个存储管理的查看面板,但是实在是太简陋,也只能提供非常简单地查找大文件的工具。

A14X

比如说从系统提供的 Storage 预览中能看到 Documents 占用的空间最多,可以点开后面的圆形 i 图标,可以看到其中占用空间很大的几个文件。

ACei

比如说对于我,就是我安装的两个虚拟机占用了比较多的空间,但这也是预想之内的。

gdu

上面的方式只能找出来系统中的大文件所在,如果我想知道每一个文件夹所占用的空间大小,我之前的文章中介绍过gdu ,这个时候就派上了用场。

brew install gdu

然后直接对想要统计的目录运行 sudo gdu ~/

ARx8

几款 macOS 上的清理工具

  • [[Clean My Mac]] 收费软件
  • [[Pretty Clean]] 免费
  • [[App Cleaner]] 免费,卸载应用
  • [[Clean Me]] 清理磁盘

Pretty Clean

PrettyClean 是一款 macOS 上的免费清理工具,界面非常简单。

AW10

App Cleaner

App Cleaner 是一款可以用来快速卸载应用以及应用相关残留文件的应用,非常小巧,但是非常强大。

AdJ6


2023-06-10 macos , cleanup , mac , gdu , linux , disk-space

使用 Listmonk 搭建自己的 Newsletter

listmonk 是一个开源的,使用 Go 语言编写的,自托管的邮件列表订阅应用。目前已经在 GitHub 收获了超过 10000 颗星星,listmon 速度非常快,功能丰富,并且可以直接打包成一个二进制文件,和 PostgreSQL 数据库一起使用。

借助 listmonk 可以非常快速的搭建属于自己的 Newsletter,Newsletter 是一种基于邮件的时事通讯,企业或组织可以通过邮箱给其成员,客户,员工或其他订阅者发送活动的新闻及广告营销的方式,但最近也逐渐成为个人出版、自媒体的流行订阅形式,相比 RSS,它更加自主,有更好的阅读体验,并且可以有更灵活的付费方式。

特性:

  • 支持公共列表和私有列表
  • 只依赖 [[PostgreSQL]]
  • 拥有管理面板
  • 基于 Go 的模板、支持 WYSIWYG 编辑器
  • 多线程,多 SMTP 邮件队列,用于快速投递邮件
  • HTTP,JSON API
  • 点击和视图追踪
  • 支持导入 [[MailChimp]] 和 [[Substack]] 的订阅用户

安装

具体的 docker-compose 可以看这里,listmonk 需要依赖一个配置文件,我一般习惯直接放在 HOME 目录中

git clone [email protected]:einverne/dockerfile.git
cd dockerfile/listmonk
cp env .env
# edit .env
# create config.toml
vi ~/listmonk/config.toml

然后填入一下内容。注意将配置文件中的内容填写,比如用户名和密码,数据库连接方式替换为自己的内容。

[app]
# Interface and port where the app will run its webserver.
address = "0.0.0.0:9000"
admin_username = "username"
admin_password = "password"

# Database.
[db]
host = "host"
port = 5432
user = "listmonk"
password = "pass"
database = "listmonk"
ssl_mode = "disable"
max_open = 25
max_idle = 25
max_lifetime = "300s"

当添加完配置文件之后,就可以使用 docker-compose up -d db 来启动数据库了,但是 listmonk 应用不回初始化数据库 Schema,所以还需要进行初始化数据库操作。

docker-compose run --rm app ./listmonk --install

等初始化数据操作完成,可以通过进入 PostgreSQL 容器查看表结构来验证。

docker exec -it listmonk_db /bin/bash

psql -d listmonk -U listmonk -W
\dt

最后就可以启动应用 docker-compose up -d

自定义静态模板文件

这部分内容已经在我上面提及的 docker-compose.yml 文件中存在。

  app:
    <<: *app-defaults
    container_name: listmonk_app
    depends_on:
      - db
    command: "./listmonk --static-dir=/listmonk/static"
    volumes:
      - "${LISTMONK_CONFIG}/config.toml:/listmonk/config.toml"
      - "./static:/listmonk/static"

导入外部订阅用户

加入已经在 [[MailChimp]] 或者 [[Substack]] 上有一定的订阅用户,那么可以通过后台工具导入 csv 文件。但需要注意的是,导入的用户默认状态是 Unconfirmed,所以需要进入数据库手动更新用户的状态。

docker exec -it listmonk_db /bin/bash
psql -d listmonk -U listmonk -W

输入密码登录数据库,然后执行 \dt 查看表。然后查看表内容

SELECT * from subscriber_lists;

然后更新所有人

UPDATE subscriber_lists SET status='confirmed' WHERE list_id=4;

设置 SSL

默认情况下 listmonk 运行在 HTTP,不提供 SSL,我们可以借助 Nginx 和 Let’s Encrypt 来生成证书提供更安全的访问。

有很多种方式可以完成

之前好几篇文章也介绍过 HestiaCP 面板中的模板文件,所以这里就简单再总结一下。

首先与进入 root 账户 sudo su -

然后进入如下的目录

cd /usr/local/hestia/data/templates/web/nginx/php-fpm/

这个目录中包含了 HestiaCP 默认的 Nginx 模板。

cp default.tpl listmonk.tpl
cp default.stpl listmonk.stpl

然后分别修改这两个新生成的 listmonk 配置文件。

将其中 location 部分修改

    location / {
        proxy_pass  http://localhost:9001;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    }

然后最好把 stpl 文件中的 proxy_hide_header Upgrade; 删除。

再进入 HestiaCP 管理后台,创建用户,然后创建网站,填入域名。创建域名进入高级管理,在 Web Template(Nginx)中选择刚刚创建的 listmonk,保存。然后在高级设置中,配置 SSL,等待获取证书,保存之后就能通过域名来访问 listmonk 了。


2023-06-09 newsletter , listmonk , email , email-server , email-service , docker , postgresql

.box 区块链上的 TLD

今天在 Twiter 上看到 .box 这个顶级域名开始开放 Early Access,然后就突然想起来之前突然某一天 Twitter 大家都换上了 .eth 的昵称。突然很多陌生的名词出现了,比如什么是 ENS,什么是 DID。

什么是 eth

.eth 是 ENS(Ethereum Name Service)域名服务提供的一种以太坊地址命名方式。它是一种去中心化的域名系统,将难以记忆的以太坊地址转换为更容易记忆的名称。.eth 域名可以被认为是一种以太坊上的虚拟资产,可以购买、出售、转让和租赁。与传统的域名系统不同,.eth 域名是在以太坊区块链上注册的,所有权也是通过智能合约进行管理和转移。

什么是 ENS

ENS 全称为 Ethereum Name Service,是以太坊上的一种域名服务,类似于互联网上的 DNS。它通过将以太坊地址映射到易于记忆的名称,使用户能够更轻松地发送和接收加密货币。通过 ENS,用户可以使用类似于“myname.eth”这样的名称来代替其以太坊地址。ENS 名称可以用于接收以太币和其他以太坊资产,还可以用于智能合约和去中心化应用程序(DApps)。

什么是 DID

DID 全称为 Decentralized Identifier,是一种去中心化身份标识符。与传统身份验证方式不同的是,DID 是基于分布式账本技术建立的,它使得用户可以在不泄露自己身份信息的情况下进行认证和授权。每个 DID 都是唯一的,且由该 DID 的所有者完全控制。DID 可以与加密货币钱包等数字身份验证系统相结合,为用户提供更安全、更私密的身份验证方式。

什么是 DAO

DAO 是“去中心化自治组织”的缩写,是一种基于区块链技术的组织形式。DAO 的核心思想是将组织的管理权和所有权分散到所有的成员手中,实现去中心化的组织管理。

DAO 是由一组智能合约和区块链技术构成的自动化组织,它们的运行是由算法和代码决定的,而不是由人类中央管理。成员可以通过投票决定组织的战略、预算、分配和其他重要事项。

DAO 的主要优点是去除了传统组织中的中心化、层级化结构,使组织更加透明、公正和开放。此外,DAO 还可以通过智能合约的执行自动化流程,以降低运营成本和提高效率。但是,DAO 也存在一些风险和挑战,例如智能合约的漏洞、成员之间的信任问题等。

什么是 DApps

DApps 全称为 Decentralized Applications,即去中心化应用程序。与传统的应用程序不同,DApps 是基于区块链技术构建的应用程序,其数据和运行代码存储在区块链网络中,而不是集中式服务器上。DApps 使用智能合约来实现代码逻辑,保证了应用程序的去中心化和安全性,同时也去除了中间商的干预。DApps 可以用于各种场景,如数字货币钱包、去中心化交易所、投票系统、游戏等。

.box

再回到本文的主角 .box 身上,box 是一个基于 Web 3.0 的顶级域名,但是 .box 兼容 Web 2.0 可以直接被访问到。我们知道在加密货币区块链的领域,每个人的帐号都是一大串几乎不能被记忆的地址。Ethereum Name Service (ENS) 就是想要扮演 Web 3.0 的 DNS 系统,让所有的地址都可以通过一次「解析」而被访问到。

box 域名可以作为 :

  • DID Profile 或者钱包
  • 去中心化的网站
  • Web 2 的电子邮件

Ethereum Name Service (ENS) 将会在今年 9 月份发布 .box 域名,注册使用 .box 域名会和 .eth 域名一样,都是通过区块链完成。

.box vs .eth

Ethereum Name Service (ENS)是一种区块链原生的域名系统,为用户在 Web3 和加密平台上提供身份认证。 它与传统域名系统有着相同的基本概念,只是使用了基于以太坊区块链的分布式技术。 ENS 的主要功能是将机器可读的标识符(如加密货币地址)转换为人类可读的域名。ENS 地址或 URL 以.eth 结尾,用简单的域名如”James.eth”取代了加密地址中的长串数字。

每个.eth 域名是用于识别以太坊地址、哈希、元数据和其他基于区块链的地址的标识符。即将推出的.box 命名系统与.eth 类似,主要区别在于.box 域名将通过 ENS 和 Web 浏览器访问,而不使用传统的 Web DNS。通过新的.box 域名,用户可以在当前使用的 Web 浏览器上访问他们的区块链地址。


2023-06-07 eth , ens , box , tld , dns

我买了一个 Ledger Nano S Plus

今天我购买了一个 Ledger Nano S Plus,是一个入门款的[[硬件钱包]](冷钱包),借此契机也正好从头开始学习和整理一下[[加密货币]]相关知识。

什么是助记词

之前已经写过一篇文章系统的讲述了什么是助记词,以及为什么可以通过助记词来回复加密货币钱包。简单来说就是助记词就是一连串特定单词组成的字符串,一般是 12 个,或 24 个单词,可以用来备份或恢复钱包的私钥。

什么是硬件钱包(冷钱包)

[[硬件钱包]] (也被称为冷钱包)是一种物理的电子设备,使用随机数生成器(RNG)生成公钥以及私钥。生成的密钥随后存入设备内,设备不会接入互联网,所以比较安全。

Ledger 拿到手之后必须要做的几个事情

照着官方的手册和在线的说明流程,下载 Ledger Live,然后用 USB 数据线连接电脑。设置 4 到 8 位 PIN 码,然后用笔在赠送的纸上写下 24 个助记词(千万记住助记词是不能接触互联网的,不管是记录到电脑中,还是拍照都是不可以的),完成设置之后,可以用 Ledger Live 下载对应的 App 到 Nano S Plus 中之后,就可以创建钱包,生成地址了。

手动恢复钱包 确认助记词

但是为了为了更安全,更熟悉地使用使用硬件钱包,我故意输错了 3 次 PIN 码,让 Ledger 重置,然后再用刚刚记下来的 24 个单词恢复钱包。这一步的目的就是为了确保我记录下来的助记词是完全正确并且可以恢复的。否则如果遇到硬件钱包丢失,或者损毁都可以不再担心,只需要保管好助记词就可以了。

充值少量的 BNB

在恢复完成之后,我又从 [[Binance]] 转出了少量的 BNB 到 Ledger 钱包中,为了确保 Ledger 钱包是完全可以正常工作的。至此初始化的过程就做完了,之后就可以正常的利用起来。

另外一个小技巧就是当只有一个 Ledger 的时候,也可以通过故意输错 PIN 的方式,让设备重置,然后重新生成一个新的助记词,那么原来的钱包还是存在的,也可以定期往其中转入加密货币,但是这个钱包就没有任何设备可以管理。于是就可以通过一个 Ledger 来生成多个钱包的目的。

另外一个需要注意的是,Ledger 生成的助记词是可以恢复到任何厂家的硬件钱包上的,但是需要查看该硬件钱包是否支持全部的币种,如果有不支持的币种,就没有办法管理了。但是不需要荒,加密货币还是在的,只需要将助记词恢复到支持的设备上即可开始管理。另外一个需要注意的是,冷钱包生成的助记词也是可以被恢复到热钱包中的,但是一旦恢复到热钱包中,那么这个钱包就不再「安全」。

最后

目前 Ledger 官方正在对彩色版本的 Ledger 进行打折销售,提供了 30% 的优惠力度,你可以通过我的邀请链接去购买,你我都可以得到价值 $10 的 BTC 返现,在选购彩色版本时,享受总价 7 折优惠,Nano X 只需要 16799 日元,Nano S Plus 只需要 8749 日元。

A5D9


2023-06-05 ledger , cryptocurrency , cryptocurrency-wallet , hardware-wallet , binance

电子书

最近文章

  • 时隔 5 年再安装 NextCloud 时隔多年,我再安装了一次 [[NextCloud]],很早之前 就在我的 QNAP TS 453B mini 上安装并使用了多年,这两年也一直在跟随着官方的版本升级 ,但是 QNAP 毕竟在局域网内,虽然可以使用 Tailscale,ZeroTier 等等工具来组件局域网,但毕竟还是不方便,最近入手一台还不错的 VPS,所以想着再搭建一个公有 IP 的 NextCloud,一方面备份一下自己的相册,另一方面也补足一下我自己使用 Syncthing时没有在线预览的页面,导致常常有些时候想访问自己的笔记而找不到。
  • 使用 SyncTV 异地远程一起看视频 因为和女朋友异地,之前我们都一直使用 [[Plex]] 来同步看视频,基本上可以做到秒级别的共享观看。今天看 GitHub 的时候发现一款叫做 SyncTV 的应用,也是可以通过同步的方式了共享观看视频流,那这样就可以一起看视频,看直播了。原来 Plex 的因为放在了一台欧洲的服务器上,在国内播放的时候总是会出现卡顿,一来是带宽限制,二来是经常发现有转码,所以常常需要我预先转码为低码率的视频才能流畅播放,出现的这个 SyncTV ,我可以搭配一台地理位置比较好的,比如日本的机器,然后通过代理的方式播放,不知道会不会提升一下使用体验。
  • 《被讨厌的勇气》读书笔记 怎么知道的这一本书
  • 修复 macOS 时区和时间错误 今天 macOS 上又发生了一个奇怪的问题,昨天晚上因为没有连接充电线,所以导致 MacBook 晚上自动关机了,看起来是因为没电自动关机了,但是早上看的时候还有一点点电,白天的时候没有任何操作就直接充电到满,但是晚一些使用的时候就发现系统的时间不太对了,在系统设置中强制同步系统时间,但没有用处,每一次同步都比当前的时间慢了一天和几个小时,甚至连分钟都对不上。于是就开始了一系列的修复过程。
  • 为播客爱好者制作的工具 Podwise Podwise 是一个针对播客的笔记软件,过去很长的一段时间里面都在寻找能够给播客记笔记的应用,之前在移动端发现了一个 [[Snipd]],可以在听到的时候,记录下播客中的精彩瞬间,还可以做笔记。但今天发现的这一款 Podwise 是基于网页的,可以在网页中播放,并且 Podwise 提供了语音转写的文字稿。