容器技术原理

2021年4月份,在byted客服做的一次部门技术分享。

1. VMs vs Containers

VMware provides one of the more elegant virtual machine definitions when they describe a VM as “a software computer.”

  • 随着物理机性能和功耗的提升,虚拟化技术的出现是为了榨干物理机资源、提升使用效率。
    • 环境隔离、更高效的服务器资源调配、改进的故障恢复能力(例如:OS重启不需要硬件自检了)
  • 通过软件模拟出一套特殊的硬件系统(hardware system),然后在其上运行操作系统。即VMware说的software computer。
    • 虚拟化的设备+特殊驱动
  • A hypervisor, or a virtual machine monitor, is software, firmware, or hardware that creates and runs VMs.
    • Hardware虚拟化:Intel® Virtualization Technology (Intel® VT) 技术,提升性能和隔离安全性

1.1 虚拟化的分类

There are two basic ways to run virtual machines: using “Type 1” virtualization or “Type 2” virtualization.

  • Type 1 virtualization, a lightweight operating system known as a hypervisor is installed on a physical computer or server. The physical box is often called “bare metal“ and Type 1 hypervisors are often called “bare metal hypervisors. hypervisor直接安装在物理机上。
  • Type 2 hypervisors, a.k.a. “hosted hypervisors,” operate a bit differently. As opposed to being installed on top of bare metal hardware, they’re installed on top of a standard operating system. hypervisor安装在操作系统之上。

type-1 vs type-2 hypervisors

1.2 虚拟化和容器的区别

  • VMs:共享基础设施,隔离OS
  • Containers:共享基础设施&OS,隔离应用
  • 相同点:isolated environment for app

VMs vs Containers

2. 什么是容器?

OS层的虚拟化,让在同一个linux kernel里的一组进程,感觉自己运行在独立的虚拟机上(实际却不是)。

OS level virtualization

  • Containers share the host kernel:不能启动不同的OS(例如:linux上跑windows容器),在host上ps能看见所有进程
  • Containers use the kernel ability to group processes for resource control
  • Containers ensure isolation through namespaces
  • Containers feel like lightweight VMs (lower footprint, faster), but are not Virtual Machines!

电影《楚门的世界》:你不是在另一个星球。

3. 容器的基石:Linux kernel features

3.1 namespaces - limits what you can see 隔离进程的工作空间

给进程提供各自独立的系统视图(system view),看不见、摸不着,从而实现隔离:

  • PID namespace for process isolation.
  • NET namespace for managing network interfaces.
  • IPC namespace for managing access to IPC resources.
  • MNT namespace for managing filesystem mount points.
  • UTS namespace for isolating kernel and version identifiers.
  • User namespace for managing a distinct set of UIDs, GIDs and capabilities.

例如 NET namespace:独立私有的network stack

  1. network interfaces
  2. routing tables
  3. iptables rules
  4. sockets (ss,netstat) :同个host上重复绑定相同的端口的能力

3.2 cgroups - limits how much you can use 限制进程的资源使用

Control cgroups, usually referred to as cgroups, are a Linux kernel feature which allow processes to be organized into hierarchical groups whose usage of various types of resources can then be limited and monitored. The kernel’s cgroup interface is provided through a pseudo-filesystem called cgroupfs. Grouping is implemented in the core cgroup kernel code, while resource tracking and limits are implemented in a set of per-resource-type subsystems (memory, CPU, and so on).

A cgroup is a collection of processes that are bound to a set of limits or parameters defined via the cgroup filesystem.

cgroups用于限制、审计(accounting for)和隔离一组进程的资源使用情况

  • Resource metering and limiting (CPU, memory, block I/O, network, etc)
  • Device node access control
  • Crowd control

3.3 unshare 命令

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
$ unshare --help
Usage:
unshare [options] <program> [<argument>...]

Run a program with some namespaces unshared from the parent.

Options:
-m, --mount[=<file>] unshare mounts namespace
-u, --uts[=<file>] unshare UTS namespace (hostname etc)
-i, --ipc[=<file>] unshare System V IPC namespace
-n, --net[=<file>] unshare network namespace
-p, --pid[=<file>] unshare pid namespace
-U, --user[=<file>] unshare user namespace
-C, --cgroup[=<file>] unshare cgroup namespace
-f, --fork fork before launching <program>
--mount-proc[=<dir>] mount proc filesystem first (implies --mount)
-r, --map-root-user map current user to root (implies --user)
--propagation slave|shared|private|unchanged
modify mount propagation in mount namespace
-s, --setgroups allow|deny control the setgroups syscall in user namespaces

-h, --help display this help and exit
-V, --version output version information and exit

For more details see unshare(1).

$ sudo unshare --fork --pid --mount-proc bash

# in container
$ ps -ef
$ cat /proc/cgroups
$ ll /proc/<pid>/ns/

4. 容器的基石:Union Filesystem

namespaces & cgroups 提供的是运行时的system view,还需要隔离文件系统:Bins/Lib。

在Docker之前,当LXC创建一个容器时会同时创建一个完整的文件系统拷贝,这会很慢且占用很多空间。Docker基于UnionFS创建一种分层的镜像格式解决了这个问题。

Wikipedia: UnionFS

Unionfs is a filesystem service for Linux, FreeBSD and NetBSD which implements a union mount for other file systems. It allows files and directories of separate file systems, known as branches, to be transparently overlaid, forming a single coherent file system. Contents of directories which have the same path within the merged branches will be seen together in a single merged directory, within the new, virtual filesystem.

镜像是一种分层只读(除了最后一层)的文件系统格式,通过镜像来实现容器初始运行环境的定制隔离:

docker-filesystems-multilayer

1
2
$ docker pull hub.byted.org/base/debian.stretch.jdk8
$ docker image history hub.byted.org/base/debian.stretch.jdk8

镜像优化tips:

  • layer堆叠:所以,在底层layer中存在的文件,即使上层layer中删除,也还是在镜像中。
  • 每层layer指令生成一个layer,指令中可以包含多个命令:优化layer大小。
  • 镜像的拉取是按layer的:GoogleContainerTools/jib 优化分层

基于镜像+UnionFS,容器实现了文件(Bins/Lib)的共享(系统)和隔离。

docker-sharing-layers

但容器运行时对于文件系统不可能只是只读,可能会修改基础镜像中已有的文件(更新软件),这又是通过copy-on-write来实现。

4.1 Copy-on-write(COW)

Copy-on-write (COW), sometimes referred to as implicit sharing or shadowing, is a resource-management technique used in computer programming to efficiently implement a “duplicate” or “copy” operation on modifiable resources.

容器读取一个在某层只读layer的文件时,可以直接读取(share),当对其修改时会拷贝后再修改(copy),同时shadow掉原来的文件:原来的还在,只是容器看不见了。

  • Sharing promotes smaller images
  • Copying makes containers efficient

5. Benefits

  • 标准化打包软件及其依赖(image):标准化运行环境,可移植性好。如java服务+jdk
  • 隔离服务运行环境(container):轻量级,高效、快速(等同于直接启动进程啊,同志们!)
  • 赋能微服务

6. Questions

  • 镜像中,文件删除怎么做到的?whiteouts
  • 容器里运行的服务需要存文件怎么办?
  • 有了容器还需要虚拟化么?
  • 容器运行的host OS,直接部署在物理机上好,还是部署在物理机上的VM之上更好?
    • 2个分别运行在同个物理机上的2个虚拟机里的进程 vs 2个运行在同个OS里的容器进程,在相互影响上有什么区别?
    • 容器内的ulimit和host的ulimit什么关系?

7. references

7.1 Containers vs VMs

7.2 Container Internals

7.3 Union Filesystem

依依成长记录2021

按农历,依依三岁了。今年外婆来北京带依依。

02/23

幼儿园还没开园,依依不用上学。爸爸妈妈要上班,所以先起床洗漱吃饭,没叫醒依依。

早上,外婆已做好了早饭,因为糖尿病不能饿肚子,已经端饭先吃了。我盛了饭刚上桌准备开吃。

依依自己醒了,走到客厅找妈妈,看见爸爸和外婆在吃饭。妈妈还在洗漱,依依没看见妈妈就要抱着再睡会,没让外婆抱,让爸爸抱。趴在爸爸肩上没说话。过了会,依依要求回房间再睡会。

到了房间,刚放床上,依依张嘴说:“奶奶在的时候,都是先喂宝宝吃饭”。原来她刚才不让外婆抱、沉默,可能是因为感到失落了-_-||。

我哈哈大笑更外婆和妈妈们说了,外婆赶紧跟依依做解释。

小家伙还会观察细节,习惯了做中心。

白天的时候,依依不听外婆话好好吃饭、收拾玩具。外婆威胁说,你这样子我不要带你了,你让奶奶来带你。依依回到:“奶奶带很多了,已经带一百八十天了。现在要外婆带了”。她还会帮着奶奶留外婆。s

高性能系统架构要点(一)

高性能系统的设计,要解决的问题可以归结到3个方面:

  • 计算
  • 存储
  • 网络

存储和网络,本质上都是IO问题,也可以简化合并为一类。

所有系统都可以抽象为一个函数:输入+计算=输出,也就是计算+IO。

1. 计算

计算要解决的问题包括:

  • 计算效率
  • 计算规模

用最短的时间最小的计算完成对于一个问题的处理,就是计算效率

例如:一项计算能用加法或乘法,那是加法还是乘法?一条指令能完成的事情,就不要用两条。耗时的计算要避免重复。

计算规模要考虑的则是怎样将大量的计算并行化,这样就能通过增加硬件来解决问题。

例如:天气预报的计算,要通过算法设计尽量将其大规模的计算过程,拆成能够并行化的多个子计算过程,类似map-reduce。高并发的请求,要尽量避免资源的排队使用,若无法避免,则通过拆锁、预分配等方式,拆成多个队列,增加队列的数量减少队列的长度,提高并行度。

所有的计算,最终都要落到CPU上执行上。那么如何衡量CPU的使用效率呢?

CPU Usage vs Load

CPU的使用效率,可以从两个方面衡量:Usage 和 Load。

  • Usage 衡量的是CPU忙碌的时间,也就是单位时间内,CPU在干活的时间占比。例如,复杂的数学计算,会很耗Usage。

  • Load 则是衡量CPU的负载压力,在一个时间点上,使用或排队等待CPU资源的进程/线程数量(OS调度的最小单位)。

例如:同样是Usage90%,A机器CPU的Load是0.1,B机器CPU的Load是10,说明B机器的CPU过载严重。

CPU的处理能力取决于两个方面:

  • 核数:决定了能够并行执行的任务的数量。
  • 速度:决定了每个核处理计算指令的速度,也就是每秒能处理指令的数量。

从系统架构层面考虑计算问题,需要掌握各阶段的计算效率、并行程度,从而判断出该系统是个什么样的系统,是计算密集型还是负载密集型,从而设计合理的部署方案,提高CPU的使用效率和系统的处理效率。同时,应该将具有不同计算特性的服务,部署在不同的物理机上,避免CPU处理效率的不稳定。

例如:用于大数据计算的服务,和用于提供rpc的服务,应该避免部署在一台机器上。计算密集型任务长时间占用CPU,会导致负载型任务等待CPU调度的时间出现波动,从而使得其响应时间出现波动甚至产生超时。

2. 存储

现今主要的存储介质包括:

  • CPU L1/L2 Cache
  • RAM 随机存取内存
  • solid state drives (SSDs) 固态硬盘
  • hard disk drives (HDDs) 硬盘

存储的访问,可以细分为寻址+数据传输,就像CPU一样,存储能否并行访问,取决于硬件的设计。

受芯片结构限制,无论是什么存储介质,都不可能无限数量的并行,就像CPU核数限制了能够并行执行的任务一样。所以,对存储的访问,最终都会出现顺序排队处理的情况。

对于内存和SSD,其支持随机存取,因此性能好,一般只需观测他们的使用率即可。为了提高他们的并行性能,可采用支持多插槽主板的方式,来接入多个设备。

对于硬盘,其内部是机械结构,读写数据时需要移动磁头进行寻址,这个过程比读取数据会更耗时,因此硬盘的随机存取性能较差。

对于小文件,存储在SSD上可以提升读写效率。硬盘更适合存取较大文件。

存储的使用效率,有两个关键指标:

  • IO处理速率:每秒处理的IO操作数量。
    • IOPS (Input/Output Operations Per Second, pronounced i-ops)
  • 数据传输速率:每秒读写的数据量大小。例如: 5mb/s

IOPS相关,有以下几个可观测的指标:

  • 负载情况:单位时间内系统提交的IO操作请求数量
  • 堆积情况:时间点上,排队等待处理的IO操作请求数量。同时衍生出队列长度、等待时间指标。
  • 处理性能:单位时间内处理完成的IO操作数量、数据量大小。

关于IOPS,Linux系统下,使用命令iostat -x可以获得以下几个指标:

  • rrqm/s The number of read requests merged per second that were queued to the device.
  • wrqm/s The number of write requests merged per second that were queued to the device.
  • r/s The number (after merges) of read requests completed per second for the device.
  • w/s The number (after merges) of write requests completed per second for the device.
  • rsec/s (rkB/s, rMB/s) The number of sectors (kilobytes, megabytes) read from the device per second.
  • wsec/s (wkB/s, wMB/s) The number of sectors (kilobytes, megabytes) written to the device per second.
  • avgrq-sz The average size (in sectors) of the requests that were issued to the device.
  • avgqu-sz The average queue length of the requests that were issued to the device.
  • awaitThe average time (in milliseconds) for I/O requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.
  • r_awaitThe average time (in milliseconds) for read requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.
  • w_awaitThe average time (in milliseconds) for write requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.

通过观察存储设备的IO处理速率和数据传输速率,可以估算出平均存取文件大小,分析出随机存储的负载情况,从而去优化程序的文件存储设计。数据传输速率太高,可以去优化程序减少传输的数据量,或者增加硬件提升带宽。

3. 网络

TODO

x.REF

工利其器:shell使用记录(2)

0. 环境要求

  • shell环境:mac 或 linux 或 cygwin

1. 命令行下转换数字的进制

相关命令:

bc - An arbitrary precision calculator language

1
2
$ echo "obase=2; ibase=16; C2" |bc
$ echo "obase=2; ibase=16; F0F0" |bc
  • obase 输出数值的进制
  • ibase 输入数值的进制

2.json转换加工

相关命令,需要安装:

jq - Command-line JSON processor

输入的json数据

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
{
"code": 0,
"message": "success",
"data": {
"success": [],
"fail": [],
"skip": [
{
"orderId": "2020020717354289900888",
"orderStatus": 1,
"merchantId": "111222",
"orderType": "9910008",
"managerId": 360119,
"cityId": 337,
"districtId": 2357,
"storeId": "x",
"creator": 360119,
"customerId": "158099739961230006",
"createdAt": "2020-02-07 17:35:43",
"updatedAt": "2020-04-16 18:31:30"
},
{
"orderId": "2020020611201319900999",
"orderStatus": 2,
"merchantId": "111333",
"orderType": "9910008",
"managerId": 122319,
"cityId": 99,
"districtId": 3049,
"storeId": "x",
"creator": 122319,
"customerId": "152647240351230043",
"createdAt": "2020-02-06 11:20:13",
"updatedAt": "2020-04-16 18:31:31"
}
]
}
}

将json转为csv

1
2
3
$ jq '.data.skip | .[] | [.orderId, .orderStatus, .orderType, .storeId, .createdAt] | join(", ")' demo.json
"2020020717354289900888, 1, 9910008, x, 2020-02-07 17:35:43"
"2020020611201319900999, 2, 9910008, x, 2020-02-06 11:20:13"

说明:

  • 使用pipe方式组合处理:|
  • json path .data.skip 取到数组
  • json path .[] 遍历数组
  • json path [.orderId, .orderStatus, .orderType, .storeId, .createdAt] 取数组元素中的相关字段
  • join(", ")是jq的内置函数,将上一个filter输出的结果join成一个字符串

3. maven采集项目依赖信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 将dependency tree以 dot 格式输出到每个项目下的 target/deps_tree.dot 文件
$ mvn compile dependency:tree -DoutputType=dot -Doutput=target/deps_tree.dot

#
$ ll **/target/*.dot
-rw-r--r-- 1 zhangjy staff 878B Jan 6 20:26 target/deps_tree.dot
-rw-r--r-- 1 zhangjy staff 1.2K Jan 6 20:26 xfund-api/target/deps_tree.dot
-rw-r--r-- 1 zhangjy staff 38K Jan 6 20:26 xfund-main/target/deps_tree.dot

$ tar cvf med_deps.tar **/target/deps_tree.dot
a target/deps_tree.dot
a xfund-api/target/deps_tree.dot
a xfund-main/target/deps_tree.dot

$ curl -F "deps=@med_deps.tar" -F "pid=sale_java/sale-boot" http://127.0.0.1:8080/collect/java

DOT(Graphviz DOT format)文件解析

我们的golang项目用的这个库:

https://github.com/awalterschulze/gographviz
Parses the Graphviz DOT language in golang

4. 按文件存储顺序拷贝文件 2021/01/10

我妈有一个简易录音机用来录佛经,文件存在sd卡上,她想拷贝多份。在mac上直接拷贝到新sd卡后,插入简易录音机播放时,发现播放顺序和原sd卡的播放顺序不一致。

简易录音机功能非常简陋,怀疑是加载文件列表时,直接按文件系统的存储顺序加载。mac下使用命令 ls -f -l 对比了新旧sd卡下文件列表的存储顺序,确认了问题。旧sd卡的第一个文件并不是文件名排序下的第一个文件。同时,新sd卡的第一个文件,也不是按文件名排序的第一个文件,mac拷贝时也未按文件名顺序写入,推测应该是多文件并行拷贝导致写入顺序随机。

想到的解决方法是:

  • 先按旧sd卡下文件的存储顺序,给文件名加自增排序前缀,按文件名固定文件顺序。
  • 拷贝到新sd卡的时候,按新文件名顺序依次拷贝,确保写入顺序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# mac 下默认按文件名排序('-1'是数字1)
$ ls -1

# 按文件在文件系统中的存储顺序列出文件名('-1'是数字1)
$ ls -f -1 RECORD0/
$ ls -f -1 RECORD0/ | grep ".mp3" > orders.txt

# 根据文件在系统中的存储顺序,给文件加上自增前缀
$ cat -n order.txt| while read n f; do echo cp -v "RECORD0/${f}" "RECORD0x/$(printf "%03d_%s" $n $f)"; done
cp -v RECORD0/REC083M.mp3 RECORD0x/001_REC083M.mp3
cp -v RECORD0/REC002M.mp3 RECORD0x/002_REC002M.mp3
cp -v RECORD0/REC003M.mp3 RECORD0x/003_REC003M.mp3
cp -v RECORD0/REC004M.mp3 RECORD0x/004_REC004M.mp3
...
# 确认生成的cp命令无误后,上述命令末尾pipe到sh执行,拷贝到了新目录RECORD0x

# 命令行下使用cp命令,按文件名顺序依次拷贝文件,保证按期望顺序写入
/Volumes $ mkdir SDCARD/RECORD0
/Volumes $ cp -v ~/Downloads/liuyf2020/RECORD0x/*.mp3 SDCARD/RECORD0

按上述方法处理后,把新sd卡插入录音机试听,确认问题fix。

word of concepts

POC

Proof of concept (POC), also known as proof of principle, is a realization of a certain method or idea in order to demonstrate its feasibility, or a demonstration in principle with the aim of verifying that some concept or theory has practical potential.[citation needed] A proof of concept is usually small and may or may not be complete.

SOP

A standard operating procedure (SOP) is a set of step-by-step instructions compiled by an organization to help workers carry out routine operations. SOPs aim to achieve efficiency, quality output and uniformity of performance, while reducing miscommunication and failure to comply with industry regulations.

The military (e.g. in the U.S. and UK) sometimes uses the term standing (rather than standard) operating procedure because a military SOP refers to a unit’s unique procedures, which are not necessarily standard to another unit. The word “standard” can imply that only one (standard) procedure is to be used across all units.

The term can also be used to refer facetiously to refer to practices that are unconstructive, yet the norm. In the Philippines, for instance, “SOP” is the term for pervasive corruption within the government and its institutions.

基础设施 SOP 设计原则

编写 SOP 的目的是指导约束主动变更(例如``计划中的维护事件)或被动变更(例如收到告警之后的响应方式)。只要是变更就一定有风险,SOP 的设计原则不是完全避免风险,而是通过谨慎的设计,让风险可预见可控制

清晰、明确,减少操作者的主观能动空间,这是 SOP 应该尽可能去靠近的方向。如果在 SOP 的指导下,操作者还需要大量现场的“随机应变”,那么就没有起到让风险“可预见”的作用。

验证和回退方案对于 SOP 的设计非常重要。不验证,不变更;不可回退,不可变更。 即使是再不变更就要发生危险的情况下,SOP 的设计者也不应该允许例外,因为解决一个风险的过程中还可能引入更多的风险。没有验证和回退方案,SOP 就不满足风险“可控制”的要求。

有些变更的确是一次性、不可逆的,例如删除文件、销毁机器。这时候 SOP 的设计者就要加强灰度手段,尽可能让风险“可预见”,在不可挽回之前暴露回来。一个例子是销毁机器之前先停止机器上全部服务,静默观察三天;再关机,静默观察一周到一个月。这样一旦触发风险需要回退,启动服务比开机快,开机比恢复数据快,满足风险逐级控制、越是可能造成大损失的动作越投入更多的灰度成本的原则。

SOP 的设计者和执行者都应当牢记墨菲定律。常在岸边走,很难不湿鞋,除非恪守原则——程序正义,不存侥幸

API vs ABI

  • API - Application Programming Interface is a compile time interface which can is used by developer to use non-project functionality like library, OS, core calls in source code
  • In computer software, an application binary interface (ABI) is an interface between two binary program modules. Often, one of these modules is a library or operating system facility, and the other is a program that is being run by a user.

Definition of API :

An API defines the interfaces by which one piece of software communicates with another at the source level.

Definition of ABI :

Whereas an API defines a source interface, an ABI defines the low-level binary interface between two or more pieces of software on a particular architecture. It defines how an application interacts with itself, how an application interacts with the kernel, and how an application interacts with libraries.

Reviewer Parlance: LGTM/SGTM/PTAL

https://github.com/golang/go/wiki/CodeReview

There are several terms code reviews may use that you should become familiar with.

  • LGTM — looks good to me
  • SGTM — sounds good to me
  • PTAL — please take a look
  • s/foo/bar/ — please replace foo with bar; this is sed syntax
  • s/foo/bar/g — please replace foo with bar throughout your entire change

RAII

Resource Acquisition Is Initialization or RAII, is a C++ programming technique which binds the life cycle of a resource that must be acquired before use (allocated heap memory, thread of execution, open socket, open file, locked mutex, disk space, database connection—anything that exists in limited supply) to the lifetime of an object.

RAII guarantees that the resource is available to any function that may access the object (resource availability is a class invariant, eliminating redundant runtime tests). It also guarantees that all resources are released when the lifetime of their controlling object ends, in reverse order of acquisition. Likewise, if resource acquisition fails (the constructor exits with an exception), all resources acquired by every fully-constructed member and base subobject are released in reverse order of initialization. This leverages the core language features (object lifetime, scope exit, order of initialization and stack unwinding) to eliminate resource leaks and guarantee exception safety. Another name for this technique is Scope-Bound Resource Management (SBRM), after the basic use case where the lifetime of an RAII object ends due to scope exit.

RAII can be summarized as follows:

  • encapsulate each resource into a class, where
  • always use the resource via an instance of a RAII-class that either

The technique was developed for exception-safe resource management in C++ during 1984–89, primarily by Bjarne Stroustrup and Andrew Koenig, and the term itself was coined by Stroustrup. RAII is generally pronounced as an initialism, sometimes pronounced as “R, A, double I”.

SLA vs. SLO vs. SLI: What’s the difference?

  • SLA: Service Level Agreements
    • An SLA (service level agreement) is an agreement between provider and client about measurable metrics like uptime, responsiveness, and responsibilities.
  • SLO: Service Level Objectives
    • An SLO (service level objective) is an agreement within an SLA about a specific metric like uptime or response time.
  • SLI: Service Level Indicator
    • An SLI (service level indicator) measures compliance with an SLO (service level objective).

SLA vs. SLO vs. SLI

基于ShardingSphere的数据分片

1. 为什么要分片

sharding是为了提升系统响应能力。对于数据库来说,为什么sharding会改善性能?

数据库管理数据提供增删改查功能,耗时的部分主要就是CPU计算和IO访问。单表记录规模过大:

  • 会增加表索引的大小,进而影响索引的IO读取和内存交换。
  • 会增加表数据的文件大小,进而影响最终数据的读取与扫描。
  • 记录数过多,在表扫描时需要处理更多未命中的数据。
  • 业务上,会提升单表操作并发压力。如果有触发表级锁的操作,会加重影响性能。

服务器单机硬件性能有上限,分表的基础上可以将表再分到不同的物理设备上。

2. 分表设计

分表的设计主要从业务需求上出发,主要是根据数据的查询特点来选择,从表操作SQL中必须的条件字段中选取。

例如:对于用户表,更新与加载主要是按用户操作,所以用户表的分表可以直接按用户id进行hash随机,用户及其关子表,都要和用户表保持一致的分表逻辑,因为用户子表的查询必然也会带着用户id。

分表基础上经常还会带着分库,分库分表后表连接查询就可能不支持了,所以这种情况下,要根据主要的使用场景来进行分表设计。

例如:用户好友关系表,存储用户与其好友的uid映射关系,主要的使用场景是加载好友列表(高频),分表设计时要以所有人用户id来进行分表。这里有一个问题,映射表里只有好友的uid,分库分表情况下又不能直接和用户表进行表连接,那么在加载好友列表时只能遍历按uid去获取好友的名称了。一种改进的方式是,在好友关系表中冗余存储需要随好友列表一起频繁加载的信息(比如姓名),附加同步机制来保证数据及时性。

3. sharding算法

将一个大表的数据拆分到多个表,数据的均匀性是非常重要的,这样才能平摊负载。在shard字段数值分布均匀的前提下,只需要按分表个数直接取模就可以了。

需要注意,最好使用一个素数取模(如果数据没有规律性,用奇数也可以),这样余数分散会较为随机分散,可以使用数据验证下。

1
2
3
4
5
SELECT (gcid %9) as p_gcid, count(1) as c1
FROM crm.customer_crm_search group by p_gcid;

SELECT (gcid %9) &7 as p_gcid, count(1) as c2
FROM crm.customer_crm_search group by p_gcid;

为了方便以后扩容调整shard数量,可以根据数据规模预估一个可能最大shard数,来获得一个较大的余数范围。然后再对余数二次shard。
例如: 先对素数127取模,余数在和7按位与。扩容倍增时,可以有规律的迁移数据,对每个旧shard只需要迁移一半的数据。

  • 7 二进制 111。扩容倍增,模数15二进制1111
  • shard0=000扩容拆成两部分:shard0=0000和shard8=1000
  • shard1=001扩容拆成两部分:shard0=0001和shard9=1001
  • etc …
    1
    2
    SELECT (gcid %127) &7 as p_gcid, count(1) as c3
    FROM crm.customer_crm_search group by p_gcid;
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
# p_gcid, c1 
0, 1262531
1, 1259903
2, 1259860
3, 1261827
4, 1261015
5, 1260104
6, 1261028
7, 1262103
8, 1262400

# p_gcid, c2
0, 2524931
1, 1259903
2, 1259860
3, 1261827
4, 1261015
5, 1260104
6, 1261028
7, 1262103

# p_gcid, c3
0, 1430624
1, 1431112
2, 1430266
3, 1430061
4, 1429165
5, 1429653
6, 1430281
7, 1339609

6. 参考

  1. Java Bean Copy框架性能对比
  2. 为什么阿里要求避免使用 Apache BeanUtils 进行属性复制?
  3. Performance of Java Mapping Frameworks
  4. MicroServices – DTO to Entity & Entity to DTO mapping – Libraries Comparison
  5. MapStruct Installation
  6. JMapper Philosophy

微服务中VO、DTO、Entity间的相互转换处理

微服务架构下,服务拆分会产生VO、DTO、Entity三类pojo:

  • VO 用于前端接口参数传递,例如用于http接口接收请求参数。可以继承扩展DTO,或者直接使用DTO。
  • DTO 用于rpc接口参数传递。单独定义,或者继承扩展其他rpc接口的DTO。
  • Entity 用于orm映射处理,与表结构一一对应,只在服务内部使用,不能对外。

1. 问题

微服务架构面向不同场景的pojo定义,引入了一个问题:一个前端请求的处理,需要在这三者之间进行转换

  • 请求:VO => DTO => Entity
  • 返回:Entity => DTO => VO

Entity

1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName(value = "orders", schema = "crazy1984")
public class Order {
@TableId(type = IdType.AUTO)
private int id;
private String orderId;
private String orderType;
private int orderStatus;
private Date createdAt;
private Date updatedAt;
}

DTO

1
2
3
4
5
6
7
@Data
public class OrderDTO {
private String orderId;
private String orderType;
private int orderStatus;
private Date createdAt;
}

VO

1
2
3
4
5
@Data
public class OrderVO extends OrderDTO{
private String orderTypeName;
private String orderStatusName;
}

2. 手动转换

直接的方法,是通过代码对pojo属性进行逐个拷贝:

1
2
3
4
5
OrderDTO dto = new OrderDTO();
dto.setOrderId(entity.getOrderId());
dto.setOrderType(entity.getOrderType());
dto.setOrderStatus(entity.getOrderStatus());
dto.setCreatedAt(entity.getCreatedAt());

这样的方式太低效,给开发人员增加许多低效的重复劳动,也不易维护(比如新增字段时,所有相关处都要同步修改)。借助IDE工具自动生成代码,可以减少重复工作。

3. 工具类辅助拷贝转换

改进一点的方法是,使用工具类进行同名属性的自动拷贝,例如使用spring的BeanUtils

1
2
OrderDTO dto = new OrderDTO();
BeanUtils.copyProperties(entity, dto);

这样可以大量减少代码工作,但也有一些不足之处:

  1. 不支持属性名映射,属性名必须完全一致。
  2. 不支持自动类型转换,spring的BeanUtils要求源属性与目标属性的类型是相互assignable的。
  3. 属性拷贝中的属性名匹配、类型检查、写权限检查都是动态判断,有性能损耗。

4. Mapping框架

除了以上方法,还可以用开源的mapping框架,如:

这些框架都支持:

  • 不同属性名的映射
  • 自动类型转换
  • 递归mapping自定义pojo的属性

例如 ModelMapper:

1
2
ModelMapper mapper = new ModelMapper();
OrderDTO dto = modelMapper.map(entity, OrderDTO.class);

这些框架的实现原理,可以归结为以下几点:

  • 基于java反射机制,根据源和目标的class自动进行属性get、set的调用。
  • 基于注解、配置,来进行不同属性名的映射、类型转换。

以上这些框架的性能对比MapStructJMapper性能较好。原因是,它们都使用代码生成将map的过程静态化了,所以实际性能和自己手写get、set一样。在开发过程中,可以通过检查生成的代码来确保mapping转换没有错误,相比ModelMapper的黑盒实现更加可靠。对于grpc协议protobuf对象和entity的互相转换,也能很好的支持。

5. MapStruct的使用

maven安装

在项目pom.xml中增加配置:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.crazy1984.mapper</groupId>
<artifactId>mapper-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>mapper-demo</artifactId>
<packaging>jar</packaging>
<properties>
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.12</org.projectlombok.version>
</properties>
<!-- provided -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.suppressGeneratorTimestamp=true</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

上述配置,能让lombok和mapstruct一起工作。

示例:Entity和DTO的双向mapping

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
/**
* demo: entity
*
* @author zhangjy
* @date 2019-10-24
*/
@Data
@TableName(value = "transfer_flow", schema = "sale_fund")
public class TransferFlow {

@TableId(type = IdType.AUTO)
private int id;
@TableField(updateStrategy = FieldStrategy.NEVER)
private String flowId;// 插入后不可更新
@TableField(updateStrategy = FieldStrategy.NEVER)
private String orderId;// 插入后不可更新
/**
* 转移状态:0未开始,1成功,2失败,3跳过
*
* @see TransferResultEnum
*/
private int transferStatus;
@TableField(updateStrategy = FieldStrategy.NEVER)
private Date createdAt;// 插入后不可更新
private Date updatedAt;
/**
* 非数据库字段:demo注释
*/
@TableField(exist = false)
private String comment;
}

/**
* demo: dto
*
* @author zhangjy
* @date 2019-10-24
*/
@Data
public class TransferFlowDTO implements Serializable {

private static final long serialVersionUID = -1256479071103445488L;
private int id;
private String flowId;
private String orderId;
/**
* 转移状态:0未开始,1成功,2失败,3跳过
*
* @see TransferResultEnum
*/
private int transferStatus;
private LocalDate createdAt;
private Date updatedAt;
/**
* demo注释
*/
private String comment;
}

/**
* @author zhangjy
* @date 2020-03-18
*/
@Mapper
public interface TransferFlowConverter {

TransferFlowConverter INSTANCE = Mappers.getMapper(TransferFlowConverter.class);

TransferFlowDTO toDTO(TransferFlow entity);

List<TransferFlowDTO> toDTO(List<TransferFlow> lst);

TransferFlow fromDTO(TransferFlowDTO dto);

}

生成的代码是:

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
public class TransferFlowConverterImpl implements TransferFlowConverter {

@Override
public TransferFlowDTO toDTO(TransferFlow entity) {
if ( entity == null ) {
return null;
}

TransferFlowDTO transferFlowDTO = new TransferFlowDTO();

transferFlowDTO.setId( entity.getId() );
transferFlowDTO.setFlowId( entity.getFlowId() );
transferFlowDTO.setOrderId( entity.getOrderId() );
transferFlowDTO.setTransferStatus( entity.getTransferStatus() );
if ( entity.getCreatedAt() != null ) {
// Date to LocalDate
transferFlowDTO.setCreatedAt( LocalDateTime.ofInstant( entity.getCreatedAt().toInstant(), ZoneOffset.UTC ).toLocalDate() );
}
transferFlowDTO.setUpdatedAt( entity.getUpdatedAt() );
transferFlowDTO.setComment( entity.getComment() );

return transferFlowDTO;
}

@Override
public List<TransferFlowDTO> toDTO(List<TransferFlow> lst) {
if ( lst == null ) {
return null;
}

List<TransferFlowDTO> list = new ArrayList<TransferFlowDTO>( lst.size() );
for ( TransferFlow transferFlow : lst ) {
list.add( toDTO( transferFlow ) );
}

return list;
}

@Override
public TransferFlow fromDTO(TransferFlowDTO dto) {
if ( dto == null ) {
return null;
}

TransferFlow transferFlow = new TransferFlow();

transferFlow.setId( dto.getId() );
transferFlow.setFlowId( dto.getFlowId() );
transferFlow.setOrderId( dto.getOrderId() );
transferFlow.setTransferStatus( dto.getTransferStatus() );
if ( dto.getCreatedAt() != null ) {
// LocalDate to Date
transferFlow.setCreatedAt( Date.from( dto.getCreatedAt().atStartOfDay( ZoneOffset.UTC ).toInstant() ) );
}
transferFlow.setUpdatedAt( dto.getUpdatedAt() );
transferFlow.setComment( dto.getComment() );

return transferFlow;
}
}

示例:带递归和额外信息补充的mapping

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
@Data
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@NoArgsConstructor
@Builder
public class TaskListDTO implements Serializable {
private static final long serialVersionUID = -2125356351064981209L;

private String taskId;
private String taskName;
private Integer taskState;
private String taskStateText;
private List<ApproverlDTO> approverlDTOList;

}

@Data
public class TaskList {
private String taskId;
private String taskName;
private Integer taskState;
private Integer rejectFlag;
private List<ApproverList> approverList;
}

/**
* 带递归和额外信息补充的mapping
* @author zhangjy
* @date 2020-03-18
*/
@Mapper
public abstract class TaskListConverter {

public static TaskListConverter INSTANCE = Mappers.getMapper(TaskListConverter.class);

@Mapping(source = "approverList", target = "approverlDTOList")
public abstract TaskListDTO toDTO(TaskList task);

public abstract List<TaskListDTO> toDTO(List<TaskList> lst);

// 递归转换主pojo内的
public abstract ApproverlDTO toDTO(ApproverList approver);

// 主pojo枚举名称补充
@AfterMapping
protected void calledWithSourceAndTarget(TaskList task,
@MappingTarget TaskListDTO.TaskListDTOBuilder target) {
if (task == null || target == null) {
return;
}
// 审批状态,1:未处理,2:已处理
if (task.getTaskState() != null) {
target.taskStateText(task.getTaskState() == 1 ? "未处理" : "已处理");
}
}

// 内部pojo枚举名称补充
@AfterMapping
protected void calledWithSourceAndTarget(ApproverList approver,
@MappingTarget ApproverlDTO.ApproverlDTOBuilder target) {
if (approver == null || target == null) {
return;
}
// 审批结果 1:同意 2:驳回
if (approver.getApproveResult() != null) {
target.approveResultText(approver.getApproveResult() == 1 ? "同意" : "驳回");
}
}

}

示例:字符串List自动转json

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
@Data
public class CustomerNoteDTO implements Serializable {
private static final long serialVersionUID = 1783984956317260001L;
private Long id;
private Long customerId;
private List<String> images;
}

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName(value = "customer_notes", schema = "crm")
public class CustomerNotes {

@TableId(type = IdType.AUTO)
private Long id;
private Long customerId;
private String images;
}

/**
* @author zhangjy
* @date 2020-03-25
*/
@ApplicationScope
@Component
@Slf4j
public class SpringMapper {
@Autowired
private Gson gson;

String map(List<String> values) {
try {
return gson.toJson(values);
} catch (Exception e) {
log.warn("list to json", e);
return null;
}
}

List<String> map(String json) {
try {
final TypeToken<?> tt = TypeToken.getParameterized(List.class, String.class);
return gson.fromJson(json, tt.getType());
} catch (Exception e) {
log.warn("json to list: {}", json, e);
return null;
}
}

}

/**
* DTO vs entity 转换器
*
* @author zhangjy
* @date 2020-03-23
*/
@Mapper(componentModel = "spring", uses = SpringMapper.class)
public interface CustomerNoteConverter {
CustomerNoteConverter INSTANCE = Mappers.getMapper(CustomerNoteConverter.class);

CustomerNotes fromCDTO(CustomerNoteDTO noteDTO);

CustomerNoteDTO toDTO(CustomerNotes note);

}

生成的代码:

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
@Component
public class CustomerNoteConverterImpl implements CustomerNoteConverter {

@Autowired
private SpringMapper springMapper;

@Override
public CustomerNotes fromCDTO(CustomerNoteDTO noteDTO) {
if ( noteDTO == null ) {
return null;
}

CustomerNotes customerNotes = new CustomerNotes();

customerNotes.setId( noteDTO.getId() );
customerNotes.setCustomerId( noteDTO.getCustomerId() );
customerNotes.setImages( springMapper.map( noteDTO.getImages() ) );

return customerNotes;
}

@Override
public CustomerNoteDTO toDTO(CustomerNotes note) {
if ( note == null ) {
return null;
}

CustomerNoteDTO customerNoteDTO = new CustomerNoteDTO();

customerNoteDTO.setId( note.getId() );
customerNoteDTO.setCustomerId( note.getCustomerId() );
customerNoteDTO.setImages( springMapper.map( note.getImages() ) );

return customerNoteDTO;
}
}

6. 参考

  1. Java Bean Copy框架性能对比
  2. 为什么阿里要求避免使用 Apache BeanUtils 进行属性复制?
  3. Performance of Java Mapping Frameworks
  4. MicroServices – DTO to Entity & Entity to DTO mapping – Libraries Comparison
  5. MapStruct Installation
  6. JMapper Philosophy

自建邮件服务器

相关知识

邮件服务器包含两大核心功能:发邮件和收邮件。

邮件投递的过程是:

  1. 发送方邮件客户端和邮件服务器通信,发送邮件。
  2. 邮件服务器收到邮件后,对邮件进行投递:如果是不同服务器的邮件地址,需要转投。
  3. 接收方邮件客户端和邮件服务器通信,收取邮件。

以上三步过程中,端与端之间都需要通信,就需要通信协议:

  1. 发送时,邮件客户端和邮件服务器间,使用的是SMTP
    • SMTP=Simple Mail Transfer Protocol,简单邮件传输协议
    • SMTP是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。
  2. 邮件服务器间转投邮件时,使用的也是SMTP
  3. 接收时,邮件客户端和邮件服务器间,使用的是POP3或IMAP
    • POP3=Post Office Protocol 3,邮局协议的第3个版本
    • IMAP=Internet Mail Access Protocol,交互式邮件存取协议

所以,搭建一个完整的邮件服务器,要包括两部分服务:

  • 发送邮件用的SMTP服务
  • 接收邮件用的POP3、IMAP服务(可选择只用一种,推荐IMAP安全性高)

部署方案

SMTP

在网络上路由和传输电子邮件的服务叫MTA(Mail Transfer Agent,邮件传输代理)。Linux下流行的开源MTA有:

  1. Sendmail 现在也叫proofpoint(被Proofpoint, Inc 收购后,已商业化),是Linux平台上最流行也是最早的MTA。配置复杂,安全机制相对较弱。
  2. Postfix 是一款流行的跨平台MTA。它的初衷是替代Sendmail,所以借鉴了Sendmail的很多功能特性,但配置更简单、性能更好。还支持一些其他特性,包括:
    • 容器支持
    • 垃圾邮件控制
    • 支持多种协议
    • 支持数据库
    • 支持信箱
    • 邮件地址操纵
  3. Exim 是一款免费的MTA,支持类Unix系统。
  4. Qmail 简单、可靠 、高效,支持安全扩展。安装简单快速,支持邮件组,支持分用户管理邮件地址列表。支持SMTP和POP3。

POP3/IMAP

用于给最终的接收方投递邮件的服务叫MDU(Mail Delivery Agent,邮件投递代理)。Linux下流行的开源MDU有:

  1. Dovecot 是类Unix平台下一款开源的IMAP和POP3服务。 它的特点是轻量级、速度快且易于安装。
  2. UW IMAP 是一款IMAP协议服务器实现参考范例,由华盛顿大学开发。

主流方案

开源邮件服务器的主流方案是Postfix+Dovecot,其中 Postfix支持SMTP协议负责发送邮件,Dovecot支持POP3+IMAP协议负责接收邮件。

部署过程可参考:在Centos7下搭建邮件服务器

容器方案

现代的邮件服务器,除了基本的收发邮件还支持许多其他功能,如:日历、web邮件、垃圾邮件过滤等。要手动一个个集成部署这些组件服务比较麻烦。在容器化技术流行的今天,有许多大神提供了容器化的解决方案:

  • Mail-in-a-Box a mail server in a box. 只有一个镜像,集成了以下组件:
  • Mailu is a simple yet full-featured mail server as a set of Docker images. 开源免费,包含多个镜像,主要特性包括:
    • Standard email server, IMAP and IMAP+, SMTP and Submission
    • Advanced email features, aliases, domain aliases, custom routing
    • Web access, multiple Webmails and administration interface
    • User features, aliases, auto-reply, auto-forward, fetched accounts
    • Admin features, global admins, announcements, per-domain delegation, quotas
    • Security, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner
    • Antispam, auto-learn, greylisting, DMARC and SPF
    • Freedom, all FOSS components, no tracker included
  • docker-mailserver A fullstack but simple mail server (smtp, imap, antispam, antivirus…). 只有配置文件,没有数据库,易于部署和升级。

DNS配置

邮件服务器部署好之后,还需要配置DNS,这样其他邮件地址服务器投递邮件时才能找到本邮件服务器。

例如:我的邮件地址是 zjy@crazy1984.com,发送方的邮件地址是fighter@163.com。那么要让163.com邮件服务器能找到crazy1984.com邮件服务器,就需要进行DNS配置。

MX

DNS配置邮件域名,要添加MX记录,可参考:How to Setup an Email Server on CentOS 7

我的MX配置:

1
2
3
记录类型:MX
主机记录:@.crazy1984.com
记录值:mx01.dm.aliyun.com

SPF

SMTP协议是个非常简单的协议,没有很好的考虑安全措施,发件人可以假冒任意地址,使得垃圾邮件和诈骗邮件横行。

SPF,全称为 Sender Policy Framework,即发件人策略框架。SPF通过查询DNS记录的方式,来检查和认证邮件发送方,因为DNS解析是不能任意假冒的。详细内容可参考:

我的SPF配置:

1
2
3
记录类型:TXT
主机记录:@.crazy1984.com
记录值:v=spf1 mx mx:mail.crazy1984.com include:spf1.dm.aliyun.com -all

以上是2018年11月前后,在研究自建邮件服务器过程中学习到的知识的一点记录,最终因为阿里云服务器端口限制问题(邮件必须relay过手审核),没有配置成功。以后找个其他云服务商试试。

依依成长记录2020

过完春节,依依三岁了。

02/17

在家办公两周,算上国内放假,一家人天天在一起一个多月了。
返京后居家观察满14天,今天周一去公司上班,妈妈继续居家办公。

傍晚的时候,依依突然说:”妈妈,天快黑了“
妈妈:”嗯,是啊“
依依:”我希望天快点黑“
妈妈:”为什么?“,心里琢磨着,依依是不是希望天黑了可以吃什么零食。
依依:”因为天黑了,爸爸就回来了“

我爱❤我的宝。

01月某天

有天,依依不听话,妈妈有点生气。
妈妈抱着依依,装着伤心的样子跟依依说:”宝宝,你是不是不爱妈妈了“
依依说:”爱啊,你看我不是抱着你吗“

05/14

姨婆今天早起赶高铁回老家了,从2月底过来到今天,陪了依依两个多月。
前两天就跟依依说过,姨婆要回老家了。依依早上起来就问,姨婆呢?
妈妈告诉她,姨婆回老家照顾弟弟妹妹了,姨婆有3个宝宝,1个弟弟2个妹妹。
依依哭了,说:“还有依依宝宝,姨婆有4个宝宝。”

命令行下批量清理redis中的错误数据

问题

程序开发过程中,因为错误代码导致在redis中写入了错误的数据,可能使得读取的时候报错。如果没有设置ttl自动过期,将长期留存在redis中。

例如,想在set的同时指定ttl,但是少给了一个TimeUnit参数:

1
redisTpl.opsForValue().set(key, siteId, expire);

使得调用方法:

1
2
3
4
// 从
void set(K key, V value, long timeout, TimeUnit unit);
// 变成
void set(K key, V value, long offset);

从而产生了垃圾数据,还不能自动过期。

KEY前缀

项目中使用redis的时候,需要严格规划KEY的前缀,这样:

  • 一来可以避免KEY冲突。
  • 二来可以按业务范围查询维护相关的KEY。

上边代码中,我们错误设置的KEY的prefix=org_site_。

清理

有了统一的KEY浅醉,我们可以用keys命令查询出所有相关的key。

1
2
3
4
5
6
7
8
redis.crazy1984.com:6379[2]> keys org_site_*
1) "org_site_12_0"
2) "org_site_12_174"
3) "org_site_12_1"
4) "org_site_344_174"
5) "org_site_344_3182"
6) "org_site_345_0"
7) "org_site_344_3178"

暴力清理

对于查出的key,如果可以全部清理,那么可以用以前介绍过的awk命令,自动生成del命令(将下边的get替换成del即可),然后执行。

1
2
3
4
5
6
7
8
9
10
$ echo "keys org_site_*" |redis-cli -h redis.crazy1984.com -n 2 --raw | awk '{print "get "$1}' | redis-cli -h redis.crazy1984.com -n 2 --raw 
12
12
1
12
12
344
1003181
345
344

条件清理

如果线上环境不允许我们暴力清理,只能定向清理错误的数据。那么如何处理呢?这时可以用上redis支持执行lua脚本的功能了。

以我们的情况为例,我们的value本想存入一个int,但是因为错误的设置,使得value的数据变长了。

比如:原来的value=123,因为错误的set把ttl作为了offset,使得实际的数据可能变成

1
2
3
4
redis.crazy1984.com:6379[2]> setrange org_site_123 100 123
(integer) 103
redis.crazy1984.com:6379[2]> get org_site_123
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00123"

正常情况下:value应该是个int,长度不会超过32 bytes。

错误情况下:因为ttl(本处使用的单位是秒)至少都是60以上,所以可以通过判断数据的长度来判断是否是错误的数据。lua脚本的参数以KEYS全局变量方式传入。

The arguments can be accessed by Lua using the KEYS global variable in the form of a one-based array (so KEYS[1], KEYS[2], …).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local del_keys = {};
local used_keys = {};
-- lua脚本的参数以KEYS传入,是个map
for i,k in ipairs(KEYS) do
-- 按字符串处理获取value的长度
local ktest = redis.call("STRLEN", k)
local kval = redis.call("GET", k)
-- 正常情况下value是个int,长度不会超过10(max=2,147,483,647)
if ktest > 10 then
-- 删除错误的数据
redis.call("DEL", k)
local stest = string.format("DEL %s %d", k, ktest)
del_keys[#del_keys+1] = {stest, kval};
else
local stest = string.format("GET %s %d", k, ktest)
used_keys[#used_keys+1] = {stest, kval};
end
end
return del_keys;

把这段脚本保存为文件 redis_clean.lua

然后再结合redis的keys命令,把所有以org_site_开头的key作为参数传给lua处理。

1
2
3
$ echo "keys org_site_*" |redis-cli -h redis.crazy1984.com -n 2 --raw | xargs redis-cli -h redis.crazy1984.comm -n 2 --eval redis_clean.lua
1) 1) "DEL org_site_123 103"
2) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00123"

问题解决。