普通视图

发现新文章,点击刷新页面。
昨天以前TripleZ's Blog

通过 systemd 服务配置链接动态库

2022年3月31日 20:31

在设置动态链接的方法中,rpath 有其自身的问题1ld.so.conf 为 OS 全局配置,可能会因为单一服务的动态库版本而影响到其他服务,不是一个优雅的方法。

那如何才能不借助 rpathld.so.conf ,即能使目标服务找到对应的动态链接库,又能不影响其他服务呢?

LD_LIBRARY_PATH

对于动态链接库的路径配置而言,除了 rpathld.so.conf ,还有 LD_LIBRARY_PATH。只要在二进制启动的环境中设置 LD_LIBRARY_PATH 变量,则 glibc 会将该变量中的路径配置作为动态链接库的查找路径之一,使得二进制可执行文件能够正常链接到其依赖的动态链接库。

LD_LIBRARY_PATH 会在当前 shell session 中“全局”生效,不过,由于笔者服务采用 systemd 来管理其生命周期,按照上述思路,应该只需要在服务启动前,将 LD_LIBRARY_PATH 注入启动环境即可,这样还能够借助 systemd 来实现一定程度的环境“隔离”。systemd 服务配置中的 Environment 字段是用于描述启动环境的环境变量的,我们先在这里加入 LD_LIBRARY_PATH (如下),重新加载 systemd 配置并重启服务,观察效果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 [Unit]
 Description=Nginx
 After=syslog.target network.target remote-fs.target nss-lookup.target

 [Service]
 User=triplez
 Group=triplez
 Type=forking
+Environment="LD_LIBRARY_PATH=/your/path/to/openssl/lib:/your/path/to/jemalloc/lib:/your/path/to/luajit/lib"
 PIDFile=/run/nginx/nginx.pid
 RuntimeDirectory=nginx
 RuntimeDirectoryMode=0755
 ExecStartPre=/your/path/to/nginx -t
 ExecStart=/your/path/to/nginx
 ExecReload=/bin/kill -s HUP $MAINPID
 ExecStop=/bin/kill -s QUIT $MAINPID

 PrivateTmp=true
 Restart=always

 [Install]
 WantedBy=multi-user.target
1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart nginx.service

事实上,对于大多数服务,只需要加入环境变量即可解决问题。但很不幸的是,由于我们的服务需要监听 1024 及以下端口(如 80、443),笔者对二进制可执行文件进行了 setcap 操作,赋予其监听低位端口的能力。

1
2
$ sudo getcap /your/path/to/nginx
/your/path/to/nginx = cap_net_bind_service+ep

Capability

由于该文件含有 capability,glibc 会将 LD_LIBRARY_PATH 忽略2 3,导致服务还是无法正确链接到动态库上。

了解原因之后,接下来要做的事情也很清晰了:一是先将二进制可执行文件上的 capability 移除(chown 就可以将其移除, setcap -ep 亦可),二是利用 systemd 的服务配置来实现配置 capability (如下)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 [Unit]
 Description=Nginx
 After=syslog.target network.target remote-fs.target nss-lookup.target

 [Service]
 User=triplez
 Group=triplez
 Type=forking
 Environment="LD_LIBRARY_PATH=/your/path/to/openssl/lib:/your/path/to/jemalloc/lib:/your/path/to/luajit/lib"
 PIDFile=/run/nginx/nginx.pid
 RuntimeDirectory=nginx
 RuntimeDirectoryMode=0755
 ExecStartPre=/your/path/to/nginx -t
 ExecStart=/your/path/to/nginx
 ExecReload=/bin/kill -s HUP $MAINPID
 ExecStop=/bin/kill -s QUIT $MAINPID

+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+AmbientCapabilities=CAP_NET_BIND_SERVICE
 PrivateTmp=true
 Restart=always

 [Install]
 WantedBy=multi-user.target

再次重新加载 systemd 配置并重启服务,问题完美解决。


  1. RpathIssue
    https://wiki.debian.org/RpathIssue ↩︎

  2. Use of file capabilities disables LD_LIBRARY_PATH - Red Hat Bugzilla
    https://bugzilla.redhat.com/show_bug.cgi?id=448594 ↩︎

  3. shared libraries - Linux capabilities (setcap) seems to disable LD_LIBRARY_PATH - Stack Overflow
    https://stackoverflow.com/questions/9843178/linux-capabilities-setcap-seems-to-disable-ld-library-path ↩︎

chaosblade 生成指定 CPU 利用率负载的原理

2022年1月26日 22:21

最近笔者在做的一个降级功能,与机器资源情况密切相关。然而在测试时发现控制 CPU 利用率来构造测试条件,并不是一个容易的事情。借助时间片的思想,笔者用一个非常简单的 shell 一定程度上解决了这个问题。但转念一想,对于混沌测试的软件,这应该是个必备能力。查找了一下 chaosblade 的相关资料,果然支持生成指定 CPU 利用率的负载。故读了读其这部分源码,看看它是怎么实现的。

使用 chaosblade 来构造指定 CPU 利用率的负载非常简单:

1
blade create cpu load --cpu-percent 80

即能够生成使 CPU 利用率到达 80% 的负载。

burncpu 的核心逻辑位于这里: https://github.com/chaosblade-io/chaosblade-exec-os/blob/318c52d83a851bc75012abc7d880d4f440f1f972/exec/bin/burncpu/burncpu.go#L140-L168

 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
func burnCpu() {

	runtime.GOMAXPROCS(cpuCount)

	var totalCpuPercent []float64
	var curProcess *process.Process
	var curCpuPercent float64
	var err error

	totalCpuPercent, err = cpu.Percent(time.Second, false)  // 获取当前所有 CPU 一秒内平均利用率
	// ...
	curProcess, err = process.NewProcess(int32(os.Getpid()))
	// ...
	curCpuPercent, err = curProcess.CPUPercent()  // 获取当前进程的 CPU 利用率
	// ...
	otherCpuPercent := (100.0 - (totalCpuPercent[0] - curCpuPercent)) / 100.0  // 除去已有进程,可操作的 CPU 利用率。值的范围为 [0, 1]
	go func() {
		t := time.NewTicker(3 * time.Second)
		for {
			select {
			// timer 3s
			case <-t.C:
				totalCpuPercent, err = cpu.Percent(time.Second, false)
				// ...
				curCpuPercent, err = curProcess.CPUPercent()
				// ...
				otherCpuPercent = (100.0 - (totalCpuPercent[0] - curCpuPercent)) / 100.0
			}
		}
	}()  // 每 3s 更新一次 totalCpuPercent, curCpuPercent 和 otherCpuPercent

	if climbTime == 0 {  // 不需要爬坡时间
		slopePercent = float64(cpuPercent)  // 爬坡值与目标值一致
	} else {
		// ...
	}

    // cpuCount 是由 runtime.NumCPU() 得来,获取的是当前 CPU 的逻辑核数量
	for i := 0; i < cpuCount; i++ {
		go func() {
			busy := int64(0)
			idle := int64(0)
			all := int64(10000000)    // 设定 10ms 为一个周期
			dx := 0.0
			ds := time.Duration(0)
			for i := 0; ; i = (i + 1) % 1000 {  // 死循环
				startTime := time.Now().UnixNano()
				if i == 0 {  // 每 1000 次进入
                    // 这个赋值语句是整个 burncpu 的灵魂。
                    // 我们最终希望获得的是 slopePercent% 的 CPU 利用率
                    // 应该生成的 CPU 压力即为 (slopePercent - totalCpuPercent[0])%
                    // 理想条件下,我们只需对 (slopePercent - totalCpuPercent[0]) 个 0.1ms 时间片设置为 busy 状态即可
                    // 但由于系统中存在其他进程,burncpu 无法真正获得到 (slopePercent - totalCpuPercent[0]) 个 0.1ms 时间片
                    // 因此需要按比例放大时间片的个数,而这个比例则是当时 burncpu 实际可用的 CPU 利用率
                    // 将这个赋值语句转换为如下方程,则更好理解了:
                    //   slopePercent  = totalCpuPercent + dx * otherCpuPercent
                    //       ^                 ^           ^                 ^
                    // 最终获得的CPU利用率 当前CPU利用率 一个周期内busy的时间片个数 burnCpu真正可操作的CPU比例
					dx = (slopePercent - totalCpuPercent[0]) / otherCpuPercent
					busy = busy + int64(dx*100000)  // 有 dx 个 0.1ms 需要为 busy 状态
					if busy < 0 {
						busy = 0
					}
					idle = all - busy
					if idle < 0 {
						idle = 0
					}
					ds, _ = time.ParseDuration(strconv.FormatInt(idle, 10) + "ns")
				}
				for time.Now().UnixNano()-startTime < busy {
				}  // 阻塞 CPU,使 CPU 位于 busy 状态,直至设定的时间片结束
				time.Sleep(ds) // 空闲 CPU,使 CPU 处于 idle 状态,直至设定的时间片结束
				runtime.Gosched()
			}
		}()
	}
	select {}  // 阻塞 burnCpu 函数,保活 goroutines
}

浅谈 beancount 借款还款交易记录方法

2021年8月10日 22:38

本文基于的假设是:友人 A 需要购买产品 B,但需要你来代他购买。探讨以下几种事件发生顺序的记账方法。

除了自己已有的 Assets:Bank:Z 外,需要额外建立这些账户:

  • Assets:Receivables:A

先转账,再交易

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
; 转账
0000-00-00 * "A transfer money"
  Assets:Receivables:A -1000.00 CNY
  Assets:Bank:Z         1000.00 CNY
  
; 此时 Assets:Bank:Z 账户起着代持资金的作用。

; 交易
0000-00-01 * "Buy B for A"
  Assets:Bank:Z         -1000.00 CNY  
  Assets:Receivables:A   1000.00 CNY
  
; 整个过程结束后各账户余额:
; Assets:Bank:Z             0.00 CNY
; Assets:Receivables:A      0.00 CNY

先交易,再转账(垫付)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
; 交易(垫付)
0000-00-00 * "Buy B for A"
  Assets:Receivables:A  1000.00 CNY
  Assets:Bank:Z        -1000.00 CNY
  
; 此时 Assets:Bank:Z 账户起着垫付资金的作用,
; Assets:Receivables:A 用于应收来自 A 的款项。

; 转账(还款)
0000-00-01 * "A transfer money"
  Assets:Bank:Z          1000.00 CNY
  Assets:Receivables:A  -1000.00 CNY
  
; 交易结束后各账户余额:
; Assets:Bank:Z             0.00 CNY
; Assets:Receivables:A      0.00 CNY

转账和交易同步

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
; 转账+交易
0000-00-00 * "A transfer money to buy B"
  Assets:Receivables:A -1000.00 CNY
  Assets:Bank:Z         1000.00 CNY
  Assets:Bank:Z        -1000.00 CNY
  Assets:Receivables:A  1000.00 CNY

; 交易结束后各账户余额:
; Assets:Bank:Z              0.00 CNY
; Assets:Receivables:A       0.00 CNY

月度记账工作流

2021年6月27日 12:57

准备工作

  • 一部装有 beancountfava 以及 double-entry-generator 的 Mac 或 PC。
  • 一部 iPhone 或 Android 手机。
  • 本地存储个人 my-bookkeepings 账本最新内容。

开始工作!

Balance CheatSheet
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
; wechat pay balance
0000-00-00 document Assets:Digital:Wechat:Cash "./path/to/your_wechat_bills.csv"
0000-00-01 balance  Assets:Digital:Wechat:Cash    <balance_value> ~ 0.00 CNY

; alipay balance
0000-00-00 document Assets:Digital:Alipay:Cash "./path/to/your_alipay_bills.csv"
0000-00-01 balance  Assets:Digital:Alipay:Cash    <balance_value> ~ 0.00 CNY

; bank balance
0000-00-01 balance Assets:Bank:CN:ICBC:Savings          <balance_value> ~ 0.00 CNY
0000-00-01 balance Assets:Bank:CN:ICBC:SocialSecurity   <balance_value> ~ 0.00 CNY
0000-00-01 balance Assets:Bank:CN:BOCOM:Savings         <balance_value> ~ 0.00 CNY
0000-00-01 balance Assets:Bank:CN:BOC:Savings           <balance_value> ~ 0.00 CNY
0000-00-01 balance Liabilities:CreditCard:CN:ICBC       <balance_value> ~ 0.00 CNY
1111-11-19 balance Liabilities:CreditCard:CN:CMB        <balance_value> ~ 0.00 CNY
1111-11-20 balance Liabilities:CreditCard:CN:BOCOM      <balance_value> ~ 0.00 CNY

导入微信支付账单

先查看上月微信 beancount 账单,确定最后导入日。

「微信」中导出从最后导入日至今日的微信支付账单。

使用如下命令生成账单:

1
2
3
4
5
double-entry-generator translate \
  --config ./config/double-entry-generator/wechat.yaml \
  --provider wechat \
  --output tmp-wechat.beancount \
  your_wechat_bills.csv

修改相关 FIXME 账户交易(posting)。

在当月 index.beancount 中添加微信支付的相关 balancedocument 语句,例:

1
2
0000-00-00 document Assets:Digital:Wechat:Cash "./path/to/your_wechat_bills.csv"
0000-00-01 balance  Assets:Digital:Wechat:Cash    <balance_value> ~ 0.00 CNY

balance 日期必须为 T+1,否则语句会忽略今日交易,导致对账失败。

若对账失败,特别是小额差异,极有可能是「零钱通」发放利息所致。值得注意的是,微信「零钱通」的利息发放并不包括在微信导出的账单当中。

值得注意的是,从「理财通」转入至「银行卡」的交易,微信支付是没有记录的,见这里

导入支付宝账单

先查看上月支付宝 beancount 账单,确定最后导入日。

「支付宝」导出从最后导入日至今日的支付宝账单。

使用如下命令生成账单:

1
2
3
4
5
double-entry-generator translate \
  --config ./config/double-entry-generator/alipay.yaml \
  --provider alipay \
  --output tmp-alipay.beancount \
  your_alipay_bills.csv

修改相关 FIXME 账户交易(posting)。

在当月 index.beancount 中添加支付宝的相关 balancedocument 语句,例:

1
2
0000-00-00 document Assets:Digital:Alipay:Cash "./path/to/your_alipay_bills.csv"
0000-00-01 balance  Assets:Digital:Alipay:Cash    <balance_value> ~ 0.00 CNY

balance 日期必须为 T+1,否则语句会忽略今日交易,导致对账失败。

导入火币账单

(可选,依据月度是否有火币交易)

先查看上月火币 beancount 账单,确定最后导入日。

「火币」导出从最后导入日至今日的火币账单。

使用如下命令生成账单:

1
2
3
4
5
double-entry-generator translate \
  --config ./config/double-entry-generator/huobi.yaml \
  --provider huobi \
  --output tmp-huobi.beancount \
  your_huobi_bills.csv

修改相关 FIXME 账户交易(posting)。

导入中国工商银行账单

先查看上月工商银行 beancount 账单,确定最后导入日。

「中国工商银行」导出从最后导入日至今日中国工商银行的借记卡和信用卡账单。

使用如下命令生成账单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 生成贷记卡账单
double-entry-generator translate \
  --config ./config/double-entry-generator/icbc-1120-5595.yaml \
  --provider icbc \
  --output tmp-icbc-1120-5595.beancount \
  your_icbc_1120_5595_bills.csv
# 生成借记卡账单
double-entry-generator translate \
  --config ./config/double-entry-generator/icbc-9855.yaml \
  --provider icbc \
  --output tmp-icbc-9855.beancount \
  your_icbc_9855_bills.csv
double-entry-generator translate \
  --config ./config/double-entry-generator/icbc-5868.yaml \
  --provider icbc \
  --output tmp-icbc-5868.beancount \
  your_icbc_5868_bills.csv

修改相关 FIXME 账户交易(posting)。

在当月 index.beancount 中添加中国工商银行的相关 balancedocument 语句,例:

1
2
3
4
0000-00-00 document Assets:Bank:CN:ICBC:Savings "./path/to/your_icbc_debit_bills.csv"
0000-00-00 document Liabilities:CreditCard:CN:ICBC "./path/to/your_icbc_credit_bills.csv"
0000-00-01 balance Assets:Bank:CN:ICBC:Savings          <balance_value> ~ 0.00 CNY
0000-00-01 balance Liabilities:CreditCard:CN:ICBC       <balance_value> ~ 0.00 CNY

导入其他银行卡账单

double-entry-generator 目前仅支持中国工商银行账单的转换,鉴于个人使用银行卡的交易较少(<10 笔/月),可以直接使用手动记账。

打开:

  • 中国工商银行
  • 招商银行
  • 中国银行
  • 交通银行

先写一个 T+1balance 对账语句,对相关的账户进行断言。

1
2
3
4
5
6
7
0000-00-01 balance Assets:Bank:CN:ICBC:Savings          <balance_value> ~ 0.00 CNY
0000-00-01 balance Assets:Bank:CN:ICBC:SocialSecurity   <balance_value> ~ 0.00 CNY
0000-00-01 balance Assets:Bank:CN:BOCOM:Savings         <balance_value> ~ 0.00 CNY
0000-00-01 balance Assets:Bank:CN:BOC:Savings           <balance_value> ~ 0.00 CNY
0000-00-01 balance Liabilities:CreditCard:CN:ICBC       <balance_value> ~ 0.00 CNY
1111-11-19 balance Liabilities:CreditCard:CN:CMB        <balance_value> ~ 0.00 CNY
1111-11-20 balance Liabilities:CreditCard:CN:BOCOM      <balance_value> ~ 0.00 CNY

笔者的信用卡账单分别是每月 1 日和 19 日出账,故将信用卡断言的日期设为当月 1 日以及 19 日。

工商银行信用卡可以看到实时余额,而招行和交行的只能在出账当天才可获得余额。因此工行信用卡可以直接断言当日余额,而招行、交行银行卡需要断言出账日与余额。

一般 0000-00-01 为记账当日 T+1 ,而 1111-11 一般为记账当月的月份。 🌰:若记账时间为 2023-01-25 ,则 0000-00-01 应改为 2023-01-261111-11 应改为 2023-01

查找当月相关银行卡账单,手动记录在当月的 index.beancount 中。

总结

以月为粒度,微信、支付宝账单基本都能够正常转换,整个工作流下来需要花费的时间成本在二十分钟以内。

使用 double-entry-generator 能够基本实现个人记账期望,可以考虑固化该工作流,以长期使用。

相关链接

  1. beancount/beancount https://github.com/beancount/beancount
  2. beancount/fava https://github.com/beancount/fava
  3. deb-sig/double-entry-generator https://github.com/deb-sig/double-entry-generator
  4. 多种支付账户的账单导出及查看方法汇总 https://blog.triplez.cn/posts/bills-export-methods/

多种支付账户的账单导出及查看方法汇总

2021年6月19日 11:05

第三方电子支付

微信支付

  1. 打开“微信”,点击“我的”,点击“支付”,进入如下界面:

  2. 点击“钱包”,点击右上角“账单”,点击右上角的“常见问题”。

  3. 点击“下载账单”。

  4. 选择“用于个人对账”。

  5. 自行选择需要导出的日期范围,填写自己的邮箱即可。

支付宝

打开「支付宝」APP,在菜单栏中点击「我的」。

进入「账单」,点击右上角「…」。

选择「开具交易流水证明」。

在「选择申请用途」中选择「用于个人对账」。

选择需要的交易时间范围。

填写电子邮箱后,即可获取账单。

余额宝

打开余额宝网站,选择日期,点击“下载”即可。

银行

借记卡

中国工商银行

登录中国工商银行个人网上银行,在「我的卡包」中选择某个银行卡,点击「明细」按钮,进入「明细查询」。

macOS 需要使用 Safari 并安装工行插件才可正常登录。

选择需要的时间区间,在「下载明细格式」选择 「EXCEL 格式(.csv)」,点击「下载」后,提示“明细正在下载中,请稍后回到本交易页面查看下载结果”。稍等片刻,页面提示“您的历史明细下载已经处理成功,请您再次进入明细查询页面进行下载!”。此时重新点击「查询」,在「下载」按钮旁边会出现可下载结果的链接,点击即可下载真正的 csv 账单文件(不要忘记重新将下载格式改为 csv)。

交通银行

手机「交通银行 APP」->查找“交易明细”->最下面“是否需要开立交易明细证明?”点击「立即申请」->「交易明细清单」->「电子版」-> 选择日期->填写电子邮箱->「确认开立」->输入银行卡密码->开立成功

PDF 为加密格式,加密密码为身份证后六位。

如何开立 CSV 格式的账单?

中国银行

手机「中国银行 APP」->「首页」->「更多」->「助手」->「交易流水打印」->「立即申请」-> 选择日期 -> 选择发送邮箱 -> 填写电子邮箱 -> 点击「申请」-> 申请成功

PDF 为加密格式,加密密码在「交易流水打印」的「申请记录」中可以找到。

网上银行: 需要安装安全输入插件,用 Chrome 登录中国银行网上银行

找不到登录账号了。

信用卡

招商银行

“电邮账单”获取方式:

掌上生活->金融->查账单->右上角…->账单服务->账单补寄->(寄送方式改为「电邮账单」)->申请补寄

zsxsoft/my-beancount-scripts

如何获取 CSV 账单?

登录网银大众版专业版后,点【信用卡→账户管理→未出账单查询/已出账单查询】查看/下载/打印一年内的账单,带有业务受理专用章。

已出账单:点击【账单明细→点击下载财务明细】打印/下载,格式为【PDF】。 未出账单:点击页面右侧的下载或打印,格式为【Excel】。

社保(深圳)

深圳五险缴费明细查询:

登录深圳社保服务网站,进入“查询服务”,选择“缴费信息查询”,点击“五险缴费明细查询”即可。

深圳医保、养老个人账户余额查询:

登录深圳社保服务网站,进入“查询服务”,选择“参保信息查询”,点击“基本医疗保险个人账户查询”即可。

公积金(深圳)

「i 深圳 APP」,在首页点击「公积金」,进入「深圳市公积金」页面,即可看到余额。点击「账户明细」即可看到缴费情况。

加密货币

火币 Global

登录火币 Global 网站,进入币币订单的成交明细页面,选择合适的时间区间后,点击成交明细右上角的导出按钮即可。

❌
❌