慎用gorm的preload加载大量关联数据

Prepared Statement contains too many placeholders

这是一个Mysql的Error。
当我们用类似 select * from users where id in (1,2,3,4,5)的时候,我们可能并没有在意这一串id到底能有多长。

实际上,这个in后面的list是有长度限制的,而且在Mysql中,这个限制无法更改,最大支持65535。

上面的query,当我们给的id太多超过这个限制的时候,就会出现
Prepared Statement contains too many placeholders
的报错了。

Why we should not abuse Gorm Preload?

1
2
3
4
5
6
7
8
9
10
11
type User Struct {
Id uint
Name string
Orders []Order `gorm:"many2many:user_orders"`
}

type Order Struct {
Id uint
Amount string
Users []User `gorm:"many2many:user_orders"`
}

上面的定义,如果我们需要找出用户id 1,2,3他们对应的所有order,可以简简单单用一个preload。

1
2
3
4
5
6
7
var users []User
var orders []Order
ids := []uint{1,2,3}
db.Preload("Orders").Find(&users, ids)
for _, user := range users {
orders = append(orders, user.orders)
}

这个乍看没啥问题呀。这个时候我们先看看gorm会去做哪几件事情。

1
2
3
// select * from users;
// select * from user_orders where user_id in (1,2,3);
// select * from orders where id in (xxx,xxx,xxx);

这个时候,如果1,2,3中间某个用户拥有大量的order,那最后的where语句就会抛出Prepared Statement contains too many placeholders的Error。

那咋办?

Work Around

直接用Raw写subquery。

1
2
3
var orders []Order
subquery := db.Raw("select order_id from user_orders where user_id in (1,3,4)")
db.Where("id in (?)", subquery).Find(&orders)

MySQL的自增ID并不一定连续

很多时候当我们给数据库表定义

1
id int auto_increment;

的时候可曾有的期待是这个id会随着插入的数据增多连续递增。

然而,递增是没有问题的,但是连续性其实并不能得到保证。

举个例子,在Gorm中,有相关Upsert的用法如下。

1
2
3
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)

这样其实在Mysql中会被映射成on duplicate的语句,这个时候本来预约给新的record的id就被消耗了一个,所以如果这次成了更新没有插入,那么这个id就被跳过了。

更多其他参考

The only guarantees from an auto_increment column (or IDENTITY in MSSQL, and the other names the concept goes by) is that each value will be unique and never smaller than a previous one: so you can rely on the values for ordering but you can not rely on them not to have gaps.
引用

DNS01验证方式手动获取Lets Encrypt 证书

背景

以前用Lets Encrypt获取ssl证书都是用最傻瓜的方式。

1
2
3
4
5
6
7
8
$ sudo letsencrypt certonly
Saving debug log to /var/log/letsencrypt/letsencrypt.log

How would you like to authenticate with the ACME CA?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Nginx Web Server plugin (nginx)
2: Spin up a temporary webserver (standalone)
3: Place files in webroot directory (webroot)

通常我会选2,然后就得把nginx先停了,然后跑一遍,然后再启动nginx。
这个其实也不麻烦,但是无论选择哪个选项,都得有一个前置条件就是本机的80端口是开放的。

这个对于vps或者公有云上的一些主机来说根本没什么问题,但是对于藏在家里路由器后面的主机,不想默认使用80端口的话,就略麻烦了。
更何况有些服务提供商禁用80端口。

这个时候,其实Lets Encrypt提供了使用DNS服务器获取证书的方式。

Lets Encrypt 验证主机身份的方式

在获取证书的时候,需要验证想要获取证书的域名的确是本人所有的,这个验证被叫Challenge。
Lets Encrypt提供了好几种验证方式,具体可以查看
Challenge Type

其中,最常见的验证方式就是之前一直用的方式,就是lets Encrypt的客户端(certbot)会在你指定的服务器目录放一个随机生成内容的文件,然后lets Encrypt会通过你指定的域名去访问你放在这个端口的文件,验证里面内容是否正确。
这种验证方式被成为HTTP-01 Challenge。

我们今天使用另外一种方式,DNS-01,手动获取证书,这个方法最大的好处是不需要开放80端口。

DNS-01 方式获取证书

首先,带参数运行certbot。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ sudo certbot certonly --manual \
> --server https://acme-v02.api.letsencrypt.org/directory \
> --preferred-challenges dns \
> -d www.bocchi.tokyo \
> -m gyorou@tjjtds.me \
> --agree-tos \
> --manual-public-ip-logging-ok
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Cert is due for renewal, auto-renewing...
Renewing an existing certificate
Performing the following challenges:
1 ---
dns-01 challenge for www.bocchi.tokyo

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.www.bocchi.tokyo with the following value:

M4v67OXrYU-nXcveB8Oajqavh6ZWY3tLUMqrZTaFjGQ

Before continuing, verify the record is deployed.

按这个指示,我们上dns的服务提供商,新建一个hostname 是_acme-challenge.www.bocchi.tokyo的TXT记录。
里面的内容就是下面这串随机生成的字符串。

过了一会儿,我们验证一下这个DNS记录有没有生效。

1
dig @01.dnsv.jp -t TXT _acme-challenge.www.bocchi.tokyo

其中01.dnsv.jp 是DNS的服务器。

当看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
~ dig @01.dnsv.jp -t TXT _acme-challenge.www.bocchi.tokyo

; <<>> DiG 9.10.6 <<>> @01.dnsv.jp -t TXT _acme-challenge.www.bocchi.tokyo
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52094
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;_acme-challenge.www.bocchi.tokyo. IN TXT

;; ANSWER SECTION:
_acme-challenge.www.bocchi.tokyo. 3600 IN TXT "M4v67OXrYU-nXcveB8Oajqavh6ZWY3tLUMqrZTaFjGQ"

;; AUTHORITY SECTION:
bocchi.tokyo. 86400 IN NS 01.dnsv.jp.
bocchi.tokyo. 86400 IN NS 02.dnsv.jp.
bocchi.tokyo. 86400 IN NS 03.dnsv.jp.
bocchi.tokyo. 86400 IN NS 04.dnsv.jp.

就知道DNS Record已经生效,这个时候,再按下会车键,Continue,验证就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Press Enter to Continue
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/www.bocchi.tokyo/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/www.bocchi.tokyo/privkey.pem
Your cert will expire on 2022-07-09. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
- If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

使用BLE Itag控制Gopro的拍摄

背景

gopro可以用语音控制。
在人流攒动的地方,说一句”gopro开始录像”,保不准你成为万众瞩目的焦点。况且,其实在室外亲测gopro有时候很难听到你说了什么,你得凑近了,大声说。

那么可不可以用手机去控制gopro通断呢? 手机上有gopro官方的app,直接可以控制gopro的各种配置以及摄影,但是一手gopro,一手手机,都得单手操作,还是十分不方便。

仔细一想,我们其实只想要一个开关按钮,当我们想要摄影时候,按下开关,gopro开始摄影,当我们想停止,再按一下,gopro停止摄影。

于是我找到了一个叫做Itag的纽扣蓝牙设备,这个设备自带一个按钮,当按下这个按钮,就会向连接这个itag的设备发送通知。

思路

按下itag按钮之后, 我们只需要在接受到这个通知的时候告诉gopro开启摄影或者停止摄影,那么我们的需求就满足了。

gopro官方最近几年推出了BLE的API,所以控制gopro的工作也不是大问题。

我们可以在IOS上用corebluetooth建立两个连接,一个连接itag,一个连接gopro,并且订阅itag上的按钮消息,当接受到按钮消息,我们发送gopro摄影的通断命令给gopro。

官方demo

只需要修改官方demo的一部分地方,增加开始摄影,停止摄影,切换摄影模式,以及监听itag按钮通知的方法。

随便建立一个新的swiftui界面,初始化两个CentralManager,在onAppear中分别扫描gopro设备和itag设备。

在ui界面上建立两个按钮,分别对应按下之后连接gopro和itag设备,同时连接完gopro切换到摄影模式,连接完itag设备开始监听button的事件,同时注册handler,如果有button事件就控制gopro进行摄影和停止摄影。

注意点

注意一定要初始化两个CentralManger,一个CenteralManger是无法同时连接两个设备的。

注意在info.plist中添加background功能,并添加”use ble accessories”,否则可能我们屏幕锁了itag的按钮就失效了。

构建家用kubernetes集群(上)

背景

之前家里仅仅放了一台Qnap的四槽NAS。用来存放一些照片,旅游的视频,绰绰有余。
傻瓜式的NAS一条龙服务的确很便利。但是一旦有了安全的问题就不是自己能够解决的了。

最近针对Qnap的勒索软件闹得沸沸扬扬,我的Qnap也不慎中招,所有小于20M的文件都被强制加密了。

好不容易打上不补丁升级系统清扫完毕,
一波未平一波又起,最近google的新闻又开始给我推送一些Qnap又遭新型勒索软件攻击的新闻。

看开是时候升级一下工具了。
于是打算整合一些二手PC,搭建一个kubetnetes集群,利用一些开源的项目,自己host所需存储服务。

硬件构成

设备 数量 价格(JPY)
intel-nuc7i7bnh 2 52,000
intel-nuc5i3ryh 1 23,000
Toshiba HDD 1T 2.5Inchi 5400rpm 3 6,900
散装 DDR4-2133 8G 5 1,5000

总共花费10万日元上下吧。

预先准备

操作系统

三台机器分别装上ubuntu server 20.04 LTS。

连接上家里的无线热点

因为家里路由器放在了电视机旁边,为了不拉拉扯扯一堆网线,准备让三台NUC通过WIFI连接到路由器。
由于ubuntu server没有图形界面,这一切需要在terminal上进行。
由于安装系统之后需要用apt install安装一些常用的工具,所以不可避免我们先得把机器用有线暂时先连接到路由器上。

1
2
sudo apt update && sudo install net-tools && \
sudo apt install wireless-tools

查看一下无线网卡接口的名称。

1
2
iwconfig
=> wlp1s0

然后打开 /etc/netplan/00-installer-config-wifi.yaml, 编辑文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
network:
version: 2
renderer: networkd
wifis:
wlp1s0:
dhcp4: no
dhcp6: no
addresses: [192.168.0.87/24]
gateway4: 192.168.0.1
nameservers:
addresses: [192.168.0.1,8.8.8.8]
access-points:
"your wifi ssid":
password: "you password"

其实已经很明明白白了,还是多解释一下,这里不使用DHCP,直接给机器绑定固定地址。因为作为K8s的node,我们不希望node的ip发生变化。
同时,得指明dns 服务器的ip,一般默认用路由器的ip就好,实在不放心,把谷歌的8.8.8.8 也放上去。

保存之后,执行sudo netplan apply, 查看一下ifconfig,默默等待无线网卡连上wifi,获取到指定的ip地址。

然后把网线拔了,继续准备下一台机器。

使用RKE安装k8s

ssh免密

ssh到每一台机器中。以下称为node。

使用RKE安装K8s的时候,需要一个用户能够ssh进入到每个节点中。假设我们已经有了这么一个用户,叫做kubeuser。

我们把mac本地的ssh公钥放入到每个node的用户home下面,以实现免密码ssh进入node。

1
2
# mac 本地
cat ~/.ssh/id_rsa.pub| pbcopy
1
2
3
4
5
# 各node
ssh-keygen
vi ~/ssh/authorized_keys
#粘贴mac本地的公钥
chmod 600 ./ssh/authorized_keys

安装docker

每个node安装docker,并把之前提到的rke需要用到的用户加到Docker用户组中。

为什么呢?因为rke需要到每个node里面执行docker,所以rke操作的用户必须不用sudo就能使用docker。

1
sudo useradd -aG docker kubeuser

创建k8s

在mac上下载rke。brew install rke

rke config 会展开一个对话模式,询问每个节点信息和你所想要的配置。

我的配置是

node ip role
k8s-1 192.168.0.87 ectd, contropanel, worker, ingress
k8s-2 192.168.0.88 worker
k8s-3 192.168.0.89 worker, ingress

配置完成之后会生成一个cluster.yml, 这个时候就可以执行rke up了。

执行完成会生成一个kube_config_cluster.yml文件,这个就是你的kubeconfig文件了。把它放到 ~/kube/config, 试着看一下是否k8s已经起来。

这里推荐k9s这个工具,我们可以直接 brew install k9s, 然后执行k9s,就可以看到我们的cluster全貌啦。

接下来

我们需要ingress-nginx实现七层LB,同时使用MentalLB实现四层LB,同时使用Longhorn快速创建Persist volumn的。

更重要一点,我们还需要将去往NAS的request转发给NAS。由于篇幅过长,就在下一篇介绍了。

这里留一点提示,三台node,只有两台配置了ingress controller, 想必有一台是要用来直接承接外部所有request的了。

Lets Encrypt是如何颁发证书的

TL;NR

分成两个步骤。第一步验证agent对domain的所有权。
第二部才是给agent的domain颁发证书。

下面假设我们的服务器(上面跑着lets encrypt的agent)的域名是example.com.

验证agent拥有domain example.com

回忆一下我们在服务器上运行certbot certonly的时候,会弹出来几个选项,其中一个是spin up a temp server。

这个就是用来完成Lets encrypt的CA服务器对我们服务器拥有example.com的验证。

具体来说,agent会按照CA服务器的吩咐,放一个文件在example.com的目录下,然后CA服务器会亲自通过example.com来获取这个文件。能够获取则证明ok,我们的agent拥有example.com这个doamin。

接下来,因为要颁发证书,所以我们的agent需要提供一对公私钥。其中公钥我们称作A。CA服务器会产生一个随机数,然后我们的agent用私钥加密了之后,和公钥A一起丢给CA服务器,CA服务器验证ok,就用这个公钥A和你进行下面一个步骤。

看这个图,钥匙指的是我们约定好的公钥。为什么是A呢?因为表示是我们agent用到的。

颁发证书

验证搞定之后,agent会发起一个CRS。这个里面包含了我们实际要给服务器签名的公钥S。除了这个公钥,还包含了一个签名,这个签名是用公钥S对应的私钥对公钥S进行了签名的产物。

然后整个CRS再用之前约定好的公钥A的私钥进行签名,交给了CA服务器。

图中钥匙S就是代表实际上我们想要颁发证书的公钥。圆圈表示签名。然后CA先验证一下用公钥A的签名,再验证一下用公钥S的私钥对公钥S的签名,都没有问题的话,颁发证书,用公钥S对证书进行加密送回给agent。

理解上面三个图需要排除一个误区,就是上面除了最后加密颁发证书之外,都是明文+签名。使用明文中的公钥就能验证签名是否是又对应的私钥签发。

如果觉得抽象就想象一下jwt,jwt谁都可以看里面的内容,至于如何验证jwt有没有被篡改,就得看jwt后面带的一串签名,大概就是同样的意思。