Akawa

ETY001的博客

之前在文章中提到过通过PHP捕获信号,以优雅的关闭程序,保证程序能够完成数据写入后才关闭。由于目前程序是通过 Docker 进行部署的,因此需要看下 Docker 容器的关闭方式。

目前 Docker 容器关闭可以通过两条命令,一条是 docker stop 一条是 docker kill

先看 docker stop

1
2
3
4
5
6
7
8
9
~/ > docker stop -h
Flag shorthand -h has been deprecated, please use --help

Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]

Stop one or more running containers

Options:
-t, --time int Seconds to wait for stop before killing it (default 10)

docker stop 命令执行的时候,会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉,因为SIGKILL信号是直接发往系统内核的,应用程序没有机会去处理它。在使用docker stop命令的时候,我们唯一能控制的是超时时间。

目前 SteemLightDB 使用 docker stop -t 60 lightdb-transfer 来停止程序,60秒的超时时间足够了。

另外一个就是 docker kill

1
2
3
4
5
6
7
8
9
~/ > docker kill -h
Flag shorthand -h has been deprecated, please use --help

Usage: docker kill [OPTIONS] CONTAINER [CONTAINER...]

Kill one or more running containers

Options:
-s, --signal string Signal to send to the container (default "KILL")

docker kill 的好处就是可以使用自定义的信号。默认信号是 SIGKILL,我们可以通过使用 -s 参数,使用指定信号。比如在我的程序中,我有对 SIGINT 信号进行处理,因此使用 docker kill -s SIGINT lightdb-transfer 也可以优雅的停止容器。

Over!!!

在部署 LightDB 第二阶段的代码的时候,需要基于 alpine 构建 PHP 镜像,结果发现 iconv 扩展有缺陷,具体表现见 https://github.com/docker-library/php/issues/240

通过该 issue 中提到的那个 hack 方法可以临时解决

1
2
RUN apk add --no-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing gnu-libiconv
ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so

但是并不是完美解决,虽然 iconv 函数可用了,但是扩展使用的库版本依旧是 unknown,并且在使用 yetanotherape/diff-match-patch 的时候,依旧有报错无法使用。( yetanotherape/diff-match-patch 在我之前的 这篇文章 里有介绍 )

基本上导致 iconv 有问题的原因,可能是 alpine 使用的是 musl ,目前基于 musl 的 iconv 库可能本身就有些问题,具体问题不详。

最终不得不改用 ubuntu 18.04 来构建,可参考我的 构建文件。使用 ubuntu 构建的最大问题就是最终的镜像包过大,目前我的这个封装,镜像包大约300多兆,之前基于 alpine 构建的话,大小在100M以内。

为了让 LightDB 在退出的时候更加安全,因此考虑加入信号处理。目前 LightDB 在主循环中,先获取区块数据,再遍历区块数据进行加工,最后入库,这几个步骤中,最不希望发生的事情就是,在加工数据最后入库的时候,程序被中断。

因此加入信号处理,对人为执行 ctrl + c 的时候,把程序中断放到获取数据阶段或者入库结束之后。

以下是示例代码

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
<?php
//使用ticks需要PHP 4.3.0以上版本
// declare(ticks = 1);

class a {
public function __construct() {
pcntl_signal(SIGTERM, array($this, 'handle'));
pcntl_signal(SIGINT, array($this, 'handle'));
}

public function handle($signo) {
var_dump($signo);
exit();
}

public function start() {
while (1) {
echo time()."\n";
sleep(1);
echo "11111111\n";
pcntl_signal_dispatch();
echo "22222222\n";
}
}

}

$b = new a();
$b->start();

PHP处理信号的方法是,把程序收到的信号入队列,然后再从队列里取信号处理。这样在使用 pcntl_signal 方法的时候,需要加上 declare(ticks = 1); 或者 pcntl_signal_dispatch();,二者添加一个即可。declare(ticks = 1); 的作用是,让 PHP 每执行一行代码就去触发下从队列取信号的操作。pcntl_signal_dispatch(); 的作用则是,主动去处理队列里的信号。如果这两句不添加一个,那么最终的执行效果是,你触发了信号,但是程序只是对信号进行入队列的操作,没有程序来执行队列中的信号。

另外一个需要注意的就是,pcntl_signal 的第二个参数的使用。网上多是面向过程的使用方法,直接写要绑定的回调函数的名字即可,PHP文档中也只是有面向过程的使用方法。经过搜索,才找到绑定对象中的函数的方法,就是上面示例代码的方法。

Over!

LightDB 开发过程中,如何把已修改的文章更新到原有文章上,是个很重要的功能。

Steem 对于大篇幅的文章修改,使用的是 patch 的方式,把增量信息存入到区块链上,这样可以很大程度上减少空间开销,但是原始数据对人类不友好。因此在最终给人阅读前,需要由程序把所有变动的 patch 都依次打上才行。

经过调研,发现 Steem 使用的是 Google 的 DiffMatchPatch 这个库,但是遗憾的是,没有针对 PHP 的封装。不过最终我也还是找到一个 PHP 的库,https://github.com/yetanotherape/diff-match-patch

这个库的使用非常的简单,直接上示例

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
<?php
require __DIR__ . '/vendor/autoload.php';
use DiffMatchPatch\DiffMatchPatch;
$dmp = new DiffMatchPatch();

$old = "[SteemLightDB](https://github.com/ety001/steem-lightdb) (以下简称 LightDB ) 是一个基于 Steem 链的 MySQL 数据服务。

在有 SBDS 的情况下,为啥还要在弄一个 SteemLightDB 呢?

由于之前搭建过 SBDS 服务,发现里面有很多的问题,其中稳定性、数据原始性、数据容量这三个问题比较突出。

* SBDS 的稳定性直接依赖于其取数据的节点的稳定性;
* SBDS 的数据太原始,加工度不够,如果应用开发者需要一些数据之间的关系,需要开发者获取后自己加工,增加了开发者的开发工作量和难度;
* SBDS 占用空间太大,对于穷人来说,服务器费用开销太大。";


$patches = "@@ -32,11 +32,8 @@
om/e
-ty0
01/s
@@ -90,17 +90,16 @@
%E6%95%B0%E6%8D%AE%E6%9C%8D%E5%8A%A1%E3%80%82%0A%0A
-%E5%9C%A8
%E6%9C%89 SBDS %E7%9A%84
";



$new_content = "[SteemLightDB](https://github.com/e01/steem-lightdb) (以下简称 LightDB ) 是一个基于 Steem 链的 MySQL 数据服务。

有 SBDS 的情况下,为啥还要在弄一个 SteemLightDB 呢?

由于之前搭建过 SBDS 服务,发现里面有很多的问题,其中稳定性、数据原始性、数据容量这三个问题比较突出。

* SBDS 的稳定性直接依赖于其取数据的节点的稳定性;
* SBDS 的数据太原始,加工度不够,如果应用开发者需要一些数据之间的关系,需要开发者获取后自己加工,增加了开发者的开发工作量和难度;
* SBDS 占用空间太大,对于穷人来说,服务器费用开销太大。";



$patches = $dmp->patch_fromText($patches);
$res = $dmp->patch_apply($patches, $old);
var_dump($res);
var_dump($new_content);

这里需要注意一点,就是 patch 中数据的格式。之前一直卡在这里调不出来的原因就是 steemd.com 中提供的 patch 数据格式有问题。就像我上面给出的示例代码中的 patch ,其中有些行前面是有空格的,而在 steemd.com 提供数据中,这些空格是消失了的。。。

除此之外,还有一个坑,就是 Steem 会针对文章长短来采取是否使用 patch。。。也就是说如果你的文章就一句话,如果你编辑修好提交了,那么就不会用 patch,而是直接把你新修改的内容直接提交保存。。。具体多长我也不清楚,我没有看源码,但是有一点可以肯定就是这个操作让人很蛋疼。目前我的解决方案是使用 try catch 来区分是否使用了 patch。

1
2
3
4
5
6
try {
$patches = $dmp->patch_fromText($patches);
$res = $dmp->patch_apply($patches, $old);
} catch(\Exception $e) {
$res = $patches;
}

虽然目前看貌似没有什么问题,但是感觉实现的很不优雅。

OVER!

SteemLightDB 的用户表设计,
之前使用的是 Many To Many self-referencing 的方法来实现的用户关注关系的数据库实现。

昨天发现在关注数据中,还有一个 what 字段我给疏忽了,这个字段表明是“关注”还是“屏蔽”,
于是想要在 join table 中加一个字段。但是看了 Doctrine 的文档后,发现并没有相关的方法,
按照 Doctrine 的意思是,ManyToMany 的 join table 的目的是为了连接两个表,
因此只会存储两个被连接表的外键字段,如果想要增加 extra column,需要自己手动建立 join table,
实现 OneToMany <=> ManyToOne 的关系。

Over!

真的是刚爬出一个坑,又跳到一个新坑。LightDBTransfer 组件使用 Symfony4 框架,感觉 Symfony3 出来都没有多久,就又出来4了。本来想使用框架来节约开发时间的,结果又额外增加了时间。

Symfony 的最新文档里,给出的在 Console 中使用数据库的最佳实践方案是,把数据库的调用封装进 Service ,然后通过容器注入,来实现最终的数据库调用。

下面是代码示例:

src/Service/ExampleManager.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace App\Service;

use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Example;

class ExampleManager
{
private $em;

public function __construct(
EntityManagerInterface $em
)
{
$this->em = $em;
}

public function findSomething($id)
{
return $this->em->getRepository(Example::class)->find($id);
}
}

src/Command/ExampleRunCommand.php

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
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

use App\Service\ExampleManager;

class TransferRunCommand extends Command
{
protected static $defaultName = 'example:run';
private $example_manager;

public function __construct(
ExampleManager $example_manager,
)
{
parent::__construct();
$this->example_manager = $example_manager;
}

protected function configure()
{
$this
->setDescription('run the example shell')
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$result = $this->example_manager->findSomething(1);
}
}

Over!!!

昨天新购置了一台大硬盘服务器,为了完成 Steem LightDB 也是拼了。今天机房那边完成了服务器的部署,登陆后发现原本 2 X 1T 的硬盘,只挂了一块,如图:

一看是通过 LVM 完成的硬盘管理。并且在 /dev/sda 这块硬盘上划出了243M做 /boot。还有两个 /dev/mapper/vg-* 的 Logical Volume。

再看下目前硬盘的各个分区以及 LVM 是怎么配置的。(注意:一定要仔细看下现有LVM的配置,我刚开始由于粗心,一直误认为PV是做在了sda上,以为sdb是空硬盘啊,差点酿成大错!!!坑爹的配置,第一次见sda上只放一个引导区的。。。

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
[root@lightdb ~]# fdisk -l

Disk /dev/sdb: 1000.2 GB, 1000204886016 bytes, 1953525168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000e0ad8

Device Boot Start End Blocks Id System
/dev/sdb1 2048 1953523711 976760832 8e Linux LVM

Disk /dev/sda: 1000.2 GB, 1000204886016 bytes, 1953525168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x00040a3c

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 514047 256000 83 Linux

Disk /dev/mapper/vg-root: 990.7 GB, 990665244672 bytes, 1934893056 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes


Disk /dev/mapper/vg-swap: 8455 MB, 8455716864 bytes, 16515072 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes


Disk /dev/mapper/vg-tmp: 1073 MB, 1073741824 bytes, 2097152 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes



### 一开始粗心没看好这里!!!也是太长时间不摸LVM了。。。

[root@lightdb ~]# pvdisplay
--- Physical volume ---
PV Name /dev/sdb1
VG Name vg
PV Size 931.51 GiB / not usable 4.00 MiB
Allocatable yes
PE Size 4.00 MiB
Total PE 238466
Free PE 1
Allocated PE 238465
PV UUID 3MQezN-W53d-oj6q-I3GK-9GdP-j4Yj-hag2Dj





[root@lightdb ~]# vgdisplay
--- Volume group ---
VG Name vg
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 4
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 3
Open LV 3
Max PV 0
Cur PV 1
Act PV 1
VG Size <931.51 GiB
PE Size 4.00 MiB
Total PE 238466
Alloc PE / Size 238465 / 931.50 GiB
Free PE / Size 1 / 4.00 MiB
VG UUID dzuyJa-LoIn-ZUAY-BvEN-X13J-EYkc-daFgy5






[root@lightdb ~]# lvdisplay
--- Logical volume ---
LV Path /dev/vg/swap
LV Name swap
VG Name vg
LV UUID qNuaiE-j4mA-5dZJ-RsZZ-YqKX-4zSm-P2vWg8
LV Write Access read/write
LV Creation host, time lightdb, 2018-05-12 15:20:38 -0400
LV Status available
# open 2
LV Size <7.88 GiB
Current LE 2016
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 253:1

--- Logical volume ---
LV Path /dev/vg/tmp
LV Name tmp
VG Name vg
LV UUID oaS1B0-eSu3-s7B4-L2Dl-EUAs-wQxU-Ueohsl
LV Write Access read/write
LV Creation host, time lightdb, 2018-05-12 15:20:39 -0400
LV Status available
# open 1
LV Size 1.00 GiB
Current LE 256
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 253:2

--- Logical volume ---
LV Path /dev/vg/root
LV Name root
VG Name vg
LV UUID wqdC7u-9sg4-ekEH-dwR5-kBpy-PdN0-TXVcN0
LV Write Access read/write
LV Creation host, time lightdb, 2018-05-12 15:20:40 -0400
LV Status available
# open 1
LV Size <922.63 GiB
Current LE 236193
Segments 1
Allocation inherit
Read ahead sectors auto
- currently set to 256
Block device 253:0

/dev/sda 这个硬盘上创建新的分区,使用剩余的全部空间

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
[root@lightdb ~]# fdisk /dev/sda
Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): t
Partition number (1,2, default 2): 2
Hex code (type L to list all codes): 8e
Changed type of partition 'Linux' to 'Linux LVM'

Command (m for help): p

Disk /dev/sda: 1000.2 GB, 1000204886016 bytes, 1953525168 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x00040a3c

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 514047 256000 83 Linux
/dev/sda2 514048 1953525167 976505560 8e Linux LVM

Command (m for help): w
The partition table has been altered!

/dev/sda2 做成 PV

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
[root@lightdb ~]# pvcreate /dev/sda2 
Physical volume "/dev/sda2" successfully created.


[root@lightdb ~]# pvdisplay
--- Physical volume ---
PV Name /dev/sdb1
VG Name vg
PV Size 931.51 GiB / not usable 4.00 MiB
Allocatable yes
PE Size 4.00 MiB
Total PE 238466
Free PE 1
Allocated PE 238465
PV UUID 3MQezN-W53d-oj6q-I3GK-9GdP-j4Yj-hag2Dj

"/dev/sda2" is a new physical volume of "<931.27 GiB"
--- NEW Physical volume ---
PV Name /dev/sda2
VG Name
PV Size <931.27 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID gGrU32-2AW0-10Sv-upON-cXEE-hqC9-fe3X76

给 VG 扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@lightdb ~]# vgextend vg /dev/sda2 
Volume group "vg" successfully extended

[root@lightdb ~]# vgdisplay
--- Volume group ---
VG Name vg
System ID
Format lvm2
Metadata Areas 2
Metadata Sequence No 6
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 3
Open LV 3
Max PV 0
Cur PV 2
Act PV 2
VG Size <1.82 TiB
PE Size 4.00 MiB
Total PE 476870
Alloc PE / Size 476870 / <1.82 TiB
Free PE / Size 0 / 0
VG UUID dzuyJa-LoIn-ZUAY-BvEN-X13J-EYkc-daFgy5

/dev/mapper/vg-root 这个 LV 扩容

1
2
3
4
[root@lightdb ~]# lvextend -l +100%FREE /dev/vg/root
Size of logical volume vg/root changed from <922.63 GiB (236193 extents) to 1.81 TiB (474598 extents).
Logical volume vg/root successfully resized.

刷新文件系统

1
2
3
4
5
[root@lightdb ~]# resize2fs /dev/vg/root
resize2fs 1.42.9 (28-Dec-2013)
Filesystem at /dev/vg/root is mounted on /; on-line resizing required
old_desc_blocks = 116, new_desc_blocks = 232
The filesystem on /dev/vg/root is now 485988352 blocks long.

查看下是否成功

1
2
3
4
5
6
7
8
9
10
[root@lightdb ~]# df -hP
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg-root 1.8T 1.2G 1.7T 1% /
devtmpfs 7.8G 0 7.8G 0% /dev
tmpfs 7.8G 0 7.8G 0% /dev/shm
tmpfs 7.8G 8.6M 7.8G 1% /run
tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
/dev/sda1 243M 148M 83M 65% /boot
/dev/mapper/vg-tmp 976M 2.6M 907M 1% /tmp
tmpfs 1.6G 0 1.6G 0% /run/user/0

根目录已经是1.8T,扩容成功!

重启服务器看了下,也没有什么异常,OVER!


扩展阅读

简单说下什么是 LVM

LVMLogical Volume Manager 的简称。可以用来在物理硬盘上创建虚拟的卷,使服务器的硬盘动态扩容变的更加轻松。

其中这里面涉及到以下几个常用概念:

  • 物理卷(PV, Physical Volume)
    物理卷就是指磁盘,磁盘分区或从逻辑上和磁盘分区具有同样功能的设备(如RAID),是LVM的基本存储逻辑块,但和基本的物理存储介质(如分区、磁盘等)比较,却包含有和LVM相关的管理参数。当前LVM允许你在每个物理卷上保存这个物理卷的0至2份元数据拷贝.默认为1,保存在设备的开始处.为2时,在设备结束处保存第二份备份.

  • 卷组(VG, Volume Group)
    LVM卷组类似于非LVM系统中的物理硬盘,其由物理卷组成。能在卷组上创建一个或多个“LVM分区”(逻辑卷),LVM卷组由一个或多个物理卷组成。

  • 逻辑卷(LV, Logical Volume)
    LVM的逻辑卷类似于非LVM系统中的硬盘分区,在逻辑卷之上能建立文件系统(比如/home或/usr等)。

其实可以简单的理解为:PV相当于是传统硬盘的扇区,VG相当于是传统硬盘,LV是在VG上划分的逻辑分区。

Over!

通过跑前几天的脚本,发现网络不稳定会导致区块数据同步不全,因此还需要写一个守护脚本,定期去检查下未完成的区块,并完成同步。

但在写的过程中发现了一个坑,目前还没有找到触发的原因,临时找到了解决方案。

下面就是简化版的问题代码:

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
#!/usr/bin/python3
#encoding:UTF-8
import sys, time
from contextlib import suppress
import pymysql
# init db
try:
conn = pymysql.connect(
host = "steem-lightdb.com",
user = "steem",
password = "steem",
database = "steemdb",
charset = 'utf8',
cursorclass = pymysql.cursors.DictCursor)
#conn.autocommit(True)
except Exception as e:
print('[warning] DB connection failed', e)
sys.exit()

def getLatestBlockNumFromDB():
global conn
sql = '''
Select block_num from blocks
Order by block_num desc limit 1;
'''
try:
with conn.cursor() as cursor:
cursor.execute(sql)
result = cursor.fetchone()
if result:
return int(result['block_num']) + 1
else:
return 1
conn.commit()
except Exception as e:
print('[warning]get latest block num error', e)
return 1

def run():
while True:
latest_block_num = getLatestBlockNumFromDB()
print(latest_block_num)
time.sleep(3)

if __name__ == '__main__':
with suppress(KeyboardInterrupt):
run()

问题 :每次获取最新的区块号,总是一样的,就像被 cache 了一样。手动 commit 没有效果。

临时解决方案 :把 autocommit 打开即可。

参考:https://github.com/PyMySQL/PyMySQL/issues/76

等以后有机会再解决吧。。。

在使用 pymysql 的时候遇到的两个坑。

第一个,在 pymysqlsql 语句中的占位符并不是 Python 中常规的占位符,sql 中的所有占位符都是 %s。如果你在 sql 中使用了类似 %d 这样的占位符,你会得到类似 TypeError: %d format: a number is required, not str 这样的错误提示。参考:https://stackoverflow.com/questions/5785154/python-mysqldb-issues-typeerror-d-format-a-number-is-required-not-str

第二个坑,依旧是使用规则上的问题,先看代码:

1
2
3
4
5
6
sql = "insert into TABLE (col_a, col_b) values (%s, %s)"
data = ("abc", "xyz")
cursor.execute(sql, data)

sql = "insert into TABLE (col_a, col_b) values ('%s', '%s')" % ("abc", "xyz")
cursor.execute(sql)

以上两种用法都是可以的,但是坑就在于不要混用,比如下面这两种情况:

1
2
3
4
5
6
sql = "insert into TABLE (col_a, col_b) values (%s, %s)" % ("abc", "xyz")
cursor.execute(sql)

sql = "insert into TABLE (col_a, col_b) values ('%s', '%s')"
data = (("abc", "xyz"), ("efg", "uvw"))
cursor.executemany(sql, data)

由于 pymysql 里有相应的机制来处理传入的 sql 语句和数据,但是机制并不完善,所以混用将会导致有多余的引号出现。

之前在家里的服务器上搭建了一个 BTS 的 API 节点,使用的是 Let’s Encrypt 的证书服务。搭建好以后,一直有一个奇怪的问题没有解决,就是在交易所使用没有问题,但是用 wscat 连接就会报 Error: unable to verify the first certificate 的错误,如下图:

使用 openssl s_client -connect bts.to0l.cn:4443 检查也是有报错的

由于不影响我使用交易所,所以就忽略了。今天 @abit 在群里说我的节点证书有问题,虽然没有指出具体问题在哪,我目测应该就是这个问题。

经过检查,证书生成没有问题,最后确定是 nginx 的证书配置有问题,参考了这个帖子 https://serverfault.com/questions/875297/verify-return-code-21-unable-to-verify-the-first-certificate-lets-encrypt-apa

就是把

1
ssl_certificate      /root/.acme.sh/bts.to0l.cn/fullchain.cer;

替换成

1
ssl_certificate      /root/.acme.sh/bts.to0l.cn/bts.to0l.cn.cer;

这次之所以出现这个问题,也是疏忽。之前在服务器上一直用 certbot 生成证书,certbot 生成证书后,会提示使用如下两个文件

1
2
/etc/letsencrypt/live/xxxx.net/fullchain.pem;
/etc/letsencrypt/live/xxxx.net/privkey.pem;

而这次使用家里搭建服务器,由于联通封锁了家庭网络的 80 和 443 ,所以没有办法用 certbot 来签发证书,因此使用的是 acme 脚本,通过 DNS 来完成证书签发过程中的域名认证。而 acme 最后生成好后,给出的两个文件是

1
2
/root/.acme.sh/bts.to0l.cn/bts.to0l.cn.cer;
/root/.acme.sh/bts.to0l.cn/bts.to0l.cn.key;

所以相当然的就配置上了。

具体的这两个证书文件为何不同还不清楚,目测 bts.to0l.cn.cer 是一个不完整的证书吧,以后有时间再去研究下。

0%