使用 Pythonheapq 模块时,如果处理的是较为复杂的数据结构,则需要实现自定义比较器来比较两个元素的大小。

使用元组

如果 heapq 中放入的是元组,那么元组的第一个元素会用于大小比较。假设有这样一个问题,给定一个数组,返回前 k 小的数字所在数组中的位置。Top k 的问题的一个解法是使用堆,但是这里要求的是数字在数组中的位置而不是数字本身,所以不能直接将数组堆化,可以先将数组中的每个数字转换成一个包含2个元素的元组,元组的第一个元素是数字本身,第二个元素则是数字在数组中的位置。

1
2
3
4
5
6
7
8
9
10
import heapq

def top_k(numbers, k):
heap = [(n, i) for i, n in enumerate(numbers)]
heapq.heapify(heap)

return list(map(lambda x: heapq.heappop(heap)[1], range(k)))

if __name__ == '__main__':
print(top_k([5, 4, 3, 2, 1], 3)) # [4, 3, 2]

实现自定义比较器

当放入堆中的是自定义类时,可以通过实现 __lt__ 方法来比较元素大小。假设有一个自定义类为 Node,它包含一个 value 属性,现在问题改为给定一个 Node 的数组,返回前 k 小的 Node 的值,可通过实现 __lt__ 方法求解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import heapq

class Node:
def __init__(self, value):
self.value = value

def __lt__(self, other):
return self.value < other.value

def top_k(nodes, k):
heap = [node for node in nodes]
heapq.heapify(heap)

return list(map(lambda x: heapq.heappop(heap).value, range(k)))

if __name__ == '__main__':
print(top_k([Node(5), Node(4), Node(3), Node(2), Node(1)], 3)) # [1, 2, 3]

参考

日常随着 Docker 的使用,Docker 会逐渐占用磁盘空间,通过 docker system df 可查看 Docker 所占用的空间:

1
2
3
4
5
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images 20 14 22.21GB 17.07GB (76%)
Containers 29 0 6.743GB 6.743GB (100%)
Local Volumes 2 0 417MB 417MB (100%)
Build Cache 0 0 0B 0B

其中 Images 表示镜像,Containers 表示容器,Local Volumes 表示本地卷,Build Cache 表示构建缓存。

整体清理

可以通过 docker system prune 进行一次空间清理:

1
2
3
4
5
6
7
WARNING! This will remove:
- all stopped containers
- all networks not used by at least one container
- all dangling images
- all dangling build cache

Are you sure you want to continue? [y/N]

该操作会删除所有停止的容器,所有未被至少一个容器使用的网络,所有的 dangling 镜像(在构建镜像时产生的 tagnone 的镜像,没有和任何其他有 tag 的镜像有关联),所有的 dangling 构建缓存(和 dangling 镜像同理)。

更激进一点,还可以执行 docker system prune -a,该操作还会删除没有和运行中的容器有关联的镜像。

镜像清理

Docker 镜像是某个应用(如数据库、某个程序语言的运行时)的磁盘快照,可以通过 docker image ls -a 查看所有的镜像(活跃的以及 dangling 的镜像):

1
2
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world latest d1165f221234 4 months ago 13.3kB

可以通过 docker image rm <name_or_id> 来删除某个镜像,支持批量删除多个镜像,多个镜像 id 之间使用空格分隔即可。不过,删除镜像要求该镜像没有被某个容器所使用,否则会提示下述类似错误:

1
2
Error response from daemon: conflict: unable to delete 4cdc5dd7eaad (must be forced) - image is being used by stopped container 3d9f62acc483
Error response from daemon: conflict: unable to delete d1165f221234 (must be forced) - image is being used by stopped container 57027ba35bdd

可以通过在执行时增加 -f 来强制删除镜像。

容器清理

容器是某个镜像的一个运行实例,可以通过 docker container ls -a 查看所有的容器:

1
2
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS                      PORTS     NAMES
3d9f62acc483 4cdc5dd7eaad "/docker-entrypoint.…" 11 minutes ago Exited (0) 11 minutes ago sleepy_babbage

要删除一个容器必须要先停止该容器(docker container stop <name_or_id>),然后通过 docker container rm <name_or_id> 删除,同样的,和删除镜像类似,该命令支持批量删除多个容器,多个容器 id 之间使用空格分隔。

网络清理

Docker 网络用于容器间的通信,它们都是一些配置文件,并不会占用多大空间,可以通过 docker network ls 查看所有的网络:

1
2
3
4
NETWORK ID     NAME      DRIVER    SCOPE
b96312481a51 bridge bridge local
85a64f881d4d host host local
e6808b80f888 none null local

可以通过 docker network rm <name_or_id> 来删除一个网络。

数据卷清理

Docker 数据卷用于持久化容器运行时保存的数据,例如通过 Docker 运行 MySQL 时指定数据卷,从而对 MySQL 的数据进行备份,可以通过 docker volume ls 查看所有的数据卷:

1
2
DRIVER    VOLUME NAME
local test-volume

同样的,可以通过 docker volume rm <name> 来删除指定的数据卷,或者使用 docker volume prune 来删除所有未和运行中的容器关联的数据卷,以及通过 docker volume prune -a 删除所有的数据卷。

最后,docker system prune -a --volumes 是在 docker system prune -a 的基础上删除所有未使用的卷。

参考

使用 SSH 连接到 Azure 的虚拟机时遇到错误:

1
2
3
4
5
6
7
8
9
➜  ~ ssh -i /path/to/some.pem xxx@x.x.x.x
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/path/to/some.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/path/to/some.pem": bad permissions
xxx@x.x.x.x: Permission denied (publickey).

这是因为创建虚拟机时从 Azure 下载的私钥默认权限太大,需要将其权限改为只读且仅当前用户可见:

1
chmod 400 some.pem

参考:

如果一个数除了1和它本身外,没有其他约数,我们称这个数为质数,但在这个定义下,1却不是质数。要回答这个问题需要先了解质数的作用,质数的主要作用在于构建欧几里得的算数基本定理:

任何一个大于1的自然数都可以唯一分解成有限个质数的乘积。

如果把1列为质数,就会破坏这种唯一性,因为在这种情况下每个自然数都有无限种分解方式,即在原有分解的基础上再乘以任意个数的1,所以1不作为质数。

参考:

使用 VisualVMVirsual GC 插件需要先和服务器建立 jstatd 连接,在 JDK 9 之前需要首先创建一个 policy 文件并声明权限:

1
2
3
grant codebase "file:${java.home}/lib/tools.jar" {
permission java.security.AllPermission;
};

然而,从 JDK 9 开始,tools.jar 已被移除,需要将 policy 文件的内容修改为:

1
2
3
4
5
6
7
grant codebase "jrt:/jdk.jstatd" {    
permission java.security.AllPermission;
};

grant codebase "jrt:/jdk.internal.jvmstat" {
permission java.security.AllPermission;
};

参考:

使用 AppVeyorVisual Studio 2019 镜像构建 Java 项目时默认使用的是 JDK 1.8这里说明了 AppVeyor 各个镜像下默认使用的 JDK 版本,虽然表格里写着 Visual Studio 2019 镜像下的默认 JDK 是1.7,不过实际是1.8),如果想更换 JDK 版本,比如更换为 JDK 11,可以重新设置 JAVA_HOMEPATH

1
2
3
before_test:
- SET JAVA_HOME=C:\Program Files\Java\jdk11
- SET PATH=%JAVA_HOME%\bin;%PATH%

完整的代码可参考 GitHub

假设有如下的 SumService

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SumService {
public static int sum(int a, int b) {
if (a <= 0) {
throw new IllegalArgumentException("a must be positive");
}

if (b <= 0) {
throw new IllegalArgumentException("b must be positive");
}

return a + b;
}
}

a 或者 b 非正数时会抛出 IllegalArgumentException 异常,由于两者抛出的是同一个异常,所以无法直接使用 expected = IllegalArgumentException.class 进行区分测试,故需要测试具体的异常信息。

使用 try/catch

用一个 try/catch 包裹测试的方法,判断抛出的异常信息:

1
2
3
4
5
6
7
@Test
public void shouldAssertExceptionMessageByAssertThrows() {
IllegalArgumentException illegalArgumentException =
Assert.assertThrows(IllegalArgumentException.class, () -> SumService.sum(0, 1));

Assert.assertEquals("a must be positive", illegalArgumentException.getMessage());
}

使用 assertThrows

借助 Assert.assertThrows 执行测试方法返回一个异常,然后判断返回的异常信息:

1
2
3
4
5
6
@Test
public void shouldAssertExceptionMessageByAssertThrows() {
IllegalArgumentException illegalArgumentException = Assert.assertThrows(IllegalArgumentException.class, () -> SumService.sum(0, 1));

Assert.assertEquals("a must be positive", illegalArgumentException.getMessage());
}

使用 ExpectedException

借助 ExpectedException 预先设定预期抛出的异常和异常信息,然后执行测试方法:

1
2
3
4
5
6
7
8
9
10
@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void shouldAssertExceptionMessageByRule() {
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("a must be positive");

SumService.sum(0, 1);
}

完整的代码可参考 GitHub

参考

问题

对于以下的异步代码:

1
2
3
4
5
public class DemoService {
public CompletableFuture<String> hello() {
return CompletableFuture.supplyAsync(() -> "hello");
}
}

我们为其编写一个测试用例,并在 CompletableFuture#whenComplete 中判断返回值是否与预期相符,然而即使返回值与预期不符,该测试也不会抛出异常:

1
2
3
4
5
6
7
8
9
@Test
public void exceptionWontBeCaptured() {
DemoService demoService = new DemoService();

demoService.hello()
.whenComplete((result, e) -> {
Assert.assertEquals("wrongValue", result);
});
}

解决

CompletableFuture#get()

我们可以借助 CompletableFuture#get() 阻塞主线程等待结果的特点,将异步代码转成同步:

1
2
3
4
5
6
@Test
public void blockMainThreadByGet() throws ExecutionException, InterruptedException {
DemoService demoService = new DemoService();

Assert.assertEquals("hello", demoService.hello().get());
}

CountDownLatch

上述方案依赖了一个具体的异步类方法,如果实际的异步类不提供相应的同步方法,上述方案则不适合。针对这种情况,可以借助 CountDownLatch,初始化一个计数为1的 CountDownLatch 的实例,在测试方法中调用 CountDownLatch#await() 方法进行等待,当异步方法执行成功后在其回调中调用 CountDownLatch#countDown() 使计数器减1变为0,从而继续执行后续的测试判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void waitOnCountDown() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
DemoService demoService = new DemoService();
AtomicReference<String> actualValue = new AtomicReference<>("");

demoService.hello()
.whenComplete((result, e) -> {
actualValue.set(result);
countDownLatch.countDown();
});

countDownLatch.await();

Assert.assertEquals("hello", actualValue.get());
}

Awaitility

Awaitility 让测试异步代码变得简单明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void poweredByAwaitility() {
DemoService demoService = new DemoService();
AtomicReference<String> actualValue = new AtomicReference<>("");

demoService.hello()
.whenComplete((result, e) -> {
actualValue.set(result);
});

await().atMost(5, SECONDS).untilAsserted(() -> {
Assert.assertEquals("hello", actualValue.get());
});
}

完整的代码可参考 GitHub

参考

问题

首先来看一个例子,构建一个 C 语言版的 hello world 镜像:

1
2
3
4
5
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}

对应的 Dockerfile 为:

1
2
3
4
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]

然后执行 docker build -t hello-world . 构建一个名为 hello-world 的镜像,然而以这种方式构建的镜像的大小竟然有1.19 GB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest b11e170bd1d2 6 minutes ago 1.19GB

因为这种构建方式生成的镜像会同时包含 gcc 镜像的内容,查看 gcc 镜像大小发现达到了1.19 GB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
gcc latest 21f378ba43ec 11 days ago 1.19GB

如果我们把基础镜像换成 Ubuntu 并安装 gcc 编译 hello.c 重新构建镜像,最后的镜像大小为213 MB:

1
2
3
4
5
FROM ubuntu
COPY hello.c .
RUN apt-get update && apt-get install gcc -y
RUN gcc -o hello hello.c
CMD ["./hello"]
1
2
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
hello-world latest 42f17d1d12a5 About a minute ago 213MB

虽然新镜像相比1.19 GB有大幅减少,但相比于 hello-world 程序本身的大小(17k)来说,213 MB依然是个庞大的数字:

1
2
$ ls hello -hl
-rwxr-xr-x 1 root root 17K Aug 4 13:54 hello

解决

Multi-stage

对于 hello-world 这个镜像来说,我们真正需要的只是最终的可执行程序,而并不关心中间的编译过程,如果能将编译阶段作为一个临时阶段而并不包含在最终的镜像中,则可有效减少最终的镜像大小。针对此,Docker 在 17.05 版本开始提供了名为 multi-stage 构建的功能。我们将原来的 Dockerfile 稍作修改,将原来的编译阶段抽取为一个 stage,然后将编译好的可执行文件复制到最终的 stage 中:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]

最终的镜像大小只有73.9 MB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest 7dd2b51c53b2 7 minutes ago 73.9MB

FROM scratch

在上一步中,我们使用 Ubuntu 作为基础镜像来运行 hello-world,相比于一个可执行程序,Ubuntu 依然过于庞大,有没有比 Ubuntu 更轻量的镜像呢?有,那就是 scratch,这表示一个空的镜像,继续将 Dockerfile 稍作修改:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM scratch
COPY --from=mybuildstage hello .
CMD ["./hello"]

最终的镜像大小只有16.4 KB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest 676253b0e9c4 31 minutes ago 16.4kB

不过在运行该镜像时却提示错误:

1
standard_init_linux.go:211: exec user process caused "no such file or directory"

这是因为这种方式构建出的镜像缺少 hello-world 运行时依赖的库。我们可以在编译 hello-world 时通过指定 -static 参数将依赖的库包含到最后的可执行文件中来解决这个问题:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c -static
FROM scratch
COPY --from=mybuildstage hello .
CMD ["./hello"]

不过包含了依赖的库后最终镜像的大小也上涨为945 KB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest e6a1fccc2de7 9 seconds ago 945kB

另外,如果不想将依赖的库包含到最终的镜像中,可以使用 busybox:glibc 这个基础镜像,该镜像包含了 C 语言的标准库,有了这个镜像在编译 hello-world 时则无需指定 -static 参数:

1
2
3
4
5
6
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM busybox:glibc
COPY --from=mybuildstage hello .
CMD ["./hello"]

不过由于该镜像本身有一定大小,最终镜像的大小达到了5.22 MB:

1
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world latest e2f2c0544800 7 seconds ago 5.22MB

总结

通过 multi-stage 构建可以有效的减少 Docker 镜像的大小,而基础镜像的选择则要具体情况分析,在满足需求的情况下选择合理的基础镜像。

参考

0%