为 WSL2 一键设置代理

在 WSL2 环境中 clone 一个很大的 git 项目,不走代理速度很慢,所以研究了一下怎么让 WSL2 走 Windows 的代理客户端。

WSL1 和 WSL2 网络的区别

在 WSL1 时代,由于 Linux 子系统和 Windows 共享了网络端口,所以访问 Windows 的代理非常简单。例如 Windows 的代理客户端监听了 8000 端口,那么只需要在 Linux 子系统中执行如下命令,就可以让当前 session 中的请求通过代理访问互联网。

1
export ALL_PROXY="http://127.0.0.1:8000"

但是 WSL2 基于 Hyper-V 运行,导致 Linux 子系统和 Windows 在网络上是两台各自独立的机器,从 Linux 子系统访问 Windows 首先需要找到 Windows 的 IP。

配置 WSL2 访问 Windows 上的代理

有两个关键步骤:

  1. WSL2 中配置的代理要指向 Windows 的 IP;
  2. Windows 上的代理客户端需要允许来自本地局域网的请求;

由于 Linux 子系统也是通过 Windows 访问网络,所以 Linux 子系统中的网关指向的是 Windows,DNS 服务器指向的也是 Windows,基于这两个特性,我们可以将 Windows 的 IP 读取出来。

例如,在 Ubuntu 子系统中,通过 cat /etc/resolv.conf 查看 DNS 服务器 IP。

1
2
# This file was automatically generated by WSL. To stop 
automatic generation of this file, add the following entry to /etc/wsl.conf:# [network]# generateResolvConf = falsenameserver 172.19.80.1

可以看到 DNS 服务器是 172.19.80.1,通过环境变量 ALL_PROXY 配置代理:

1
export ALL_PROXY="http://172.19.80.1:7890"

7890 是 Windows 上运行的代理客户端的端口,记得要在 Windows 代理客户端上配置允许本地局域网请求。

一键配置脚本

将上面的过程写入一个 bash 脚本,可以轻松的实现一键配置代理:

1
2
3
#!/bin/bash
host_ip=$(cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " ")
export ALL_PROXY="http://$host_ip:7890"

脚本通过 cat /etc/resolv.conf 来获取 DNS 服务器,也就是 Windows 的 IP,再将其中的 IP 部分截取出来,加上代理客户端的端口(我的是 7890,可以根据自己实际情况修改),使用 export 写入环境变量中。

脚本也可以从这里下载.proxyrc,使用时只需要 source .proxyrc 就可以生效。

面向开发者的 WSL2 安装指南

为什么要使用 Windows 做开发

一直以来 macOS 以类 unix 的特性,获得了程序员的青睐,但是近几年 Apple 在硬件领域少有让人耳目一新的产品,加上取消 Esc、使用蝶式键盘、基本为0的硬件可升级性、系统权限的收紧等骚操作,让 Mac 从软件到硬件都不如以前适合编程。另一方面,PC 阵营在软件层面保持开放的基础上,硬件体验也逐步赶上甚至超越 Mac,我也不想在用 Mac 做开发,用 PC 玩游戏,希望用一台电脑兼顾游戏和开发,所以选择回到了 PC 阵营。

随着微软拥抱开源领域,Windows 也开始变得程序员友好。Windows 10 2004 发布后,WSL2 也可以在正式版 Windows 10 中使用,相比于 macOS,WSL2 是一个原生 Linux 环境而非类 unix 环境,甚至可以在 App 商店中选择所需要的发行版。而相比于 WSL1,WSL2 采用了 HyperV 虚拟机的方式,解决了 WSL1 不能安装 Docker 等问题。

WSL1 和 WSL2

相比于 WSL1,WSL2 通过虚拟机的方式带来了更完整的 Linux 内核,但这种方式也引入了一些问题,微软给出了下面的图表来展示这些不同:

WSL2 不能和 VMWarework Station、VirtualBox 同时运行这一条已经过时了,VirtualBox 和 VMWare Workstation 都发布了支持 WSL2 和 Hyper-V 的新版。

WSL2 跨 OS 的磁盘性能的确低的令人发指,尤其是小文件,安装一个 Python 依赖可能需要等几分钟,安装一个 NodeJS 依赖…当我没说…相关 ISSUE 底下有无数吐槽,不过目前还没有人出来解决,在这里:https://github.com/microsoft/WSL/issues/4197)。不过微软在文档里也说了,有很多方式可以避免跨 OS 使用文件系统,比如使用 VSCode 的 remote deployment 功能,我认为这也是更好的实践,并且也这么做了。当然,如果你必须跨 OS 使用文件系统,还是乖乖留在 WSL1 吧(WSL1 太渣了,还是留在 VirtualBox 吧)。

除了表格里说的,我认为 WSL2 最大的问题在于…资源占用太大了…我是一台 16G 内存的电脑,使用 WSL2 后开机就要吃掉 11G 的内存,虽然可以使用 wsl --shutdown 关闭虚拟机释放资源,但是相比于 VirtualBox 和 WSL1,WSL2 的资源占用可以说翻倍了。

微软提供了限制 WSL2 资源的方式,参见https://docs.microsoft.com/en-us/windows/wsl/release-notes#build-18945

要做些什么

好了,搞清楚 WSL2 是什么之后接下来的问题就是怎么做了,这篇文章会指导你完成如下的安装流程:

  • 升级 Windows 10 到 2004
  • 启用 WSL2 并安装 Linux

以及一些最佳实践:

  • 网络互通
  • 文件系统互通
  • 使用 Docker

最后会聊聊 WSL2 的未来。

升级 Windows 10 2004

升级到 Windows 10 2004 有多种方法,最靠谱的还是从设置 - 更新和安全里进行 OTA 升级,但是 Windows 的更新是分批推送的,2004 更新有可能等到一两个月后才会出现在你的更新界面中。

另一种更快速的方法是从官方地址下载镜像升级,访问这个地址下载运行就可以升级到最新的版本 https://www.microsoft.com/software-download/windows10,需要注意的是,发布初期可能 bug 含量会高一些,介意的还是再等等。

启用 WSL2

升级 Windows 10 的过程不会碰到太多问题,升级后还需要进行一些配置才可以使用 WSL2,首先要启用 Windows 子系统功能,使用管理员权限打开一个 PowerShell 窗口,输入以下命令,并重启系统:

1
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

重启后,Windows 默认启用的是 WSL1,还需要再启用虚拟机平台功能,在 PowerShell 中输入以下命令,并再次重启系统:

1
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

重启后,在 PowerShell 中输入以下命令,将 WSL 默认版本改为 WSL2:

1
wsl --set-default-version 2

接下来在 Microsoft Store 中找一个 Linux 发行版进行安装,安装后在 PowerShell 中执行 wsl -l -v 可以看到当前的发行版是否跑在 WSL2 中,如果显示版本是1…请重复上面的安装步骤。

设置 WSL 版本为 WSL2 报错时,请访问 https://docs.microsoft.com/en-us/windows/wsl/wsl2-kernel 下载 WSL2 Kernel。

网络互通

WSL2 不和 Windows 共享一个 localhost,所以不像 WSL1 中 Linux 和 Windows 有无感知的网络互通性。

Windows 访问 WSL2 启动的网络服务,可以直接使用 localhost,但是 Linux 访问 Windows 启动的网络服务这种方式就不行了,可以使用如下脚本获取 Windows 的 IP,并使用 IP 访问 Windows:

1
ip route | grep default | awk '{print $3}'

文件系统互通

WSL2 访问 Windows 文件系统依然通过挂载分区的方式,Windows 下的磁盘会被挂载在 /mnt 下,例如 /mnt/c

相比于 WSL1,这次增加了 Windows 访问 Linux 分区的能力,可以在资源管理器中输入 \\wsl$\<子系统名> 访问对应的子系统分区,为了方便也可以在资源管理器中把 Linux 分区挂载成一个磁盘。

更加方便的一个方式是,在 Terminal 中,使用 explorer.exe . 可以直接调用资源管理器打开当前目录,有点类似 Mac 下的 open .

使用 Docker

WSL2 带来了完整的 Linux 内核,所以可以参照 Docker 在 Linux 上安装的过程安装 Docker,并且可以正常使用,但是会有一个小瑕疵。WSL2 的 systemd 吧…还不是原生的,也就是在 WSL1 上就有的无法启动服务守护进程的问题依然存在,Docker 服务自然也不能幸免。

解决这个问题有三种方案:

  • 可以让 Windows 执行开机脚本,通过脚本启动 WSL2 中的 Docker,参见https://blog.csdn.net/XhyEax/article/details/105560377,这种方案在 WSL1 时代就有,我自己使用过没碰到过问题。

  • 第二种方案是使用第三方工具运行 systemd,参见 https://github.com/arkane-systems/genie,原理是提供了一个单独的 namespace 跑 systemd。

  • 第三种方案是使用 Docker Desktop,2.3.0.2 以上版本已经支持 WSL2 和 Hyper-V,免去一些折腾。

GPU 支持

Build 2020 发出消息说 WSL2 要支持 GPU,Nvidia 也发出了 CUDA on WSL2 的预览版,在这里:https://developer.nvidia.com/cuda/wsl

这意味着以后可以直接在 Windows 上使用 GPU 加速训练模型了,虽然支持成都还很有限,但是相比于 Mac 可领先了一个身位都不只了。

总结

在体系结构上 WSL2 没有太大的创新,本质就是一台跑在 Hyper-V 上的虚拟机,你可以使用 VMWare Workstation 或 VirtualBox,经过简单的配置实现其 100% 的功能,并且会有更高的性能,更少的资源占用,但是毕竟 WSL2 是一项原生功能,其配置上的便利性和兼容性,大大减少了使用者花在配置上的精力,最大程度的实现开箱即用,这也是 Apple 一直以来吸引开发者的主要原因。在这个时间点,使用 Windows + WSL2 来构建开发环境已经可以满足大部分的需求。

参考

https://github.com/microsoft/WSL/issues/4197

https://docs.microsoft.com/en-us/windows/wsl/wsl2-kernel

https://docs.microsoft.com/en-us/windows/wsl/install-win10

https://github.com/arkane-systems/genie

https://docs.microsoft.com/en-us/windows/wsl/release-notes#build-18945

https://developer.nvidia.com/cuda/wsl

https://devblogs.microsoft.com/directx/directx-heart-linux/

gRPC 跨进程使用引发的问题

问题描述

在 Python 项目中使用 gRPC 进行通信,跨进程使用时,会出现阻塞或报错的情况(根据 gRPC.io 的版本不同,现象不同)。下面代码展示了一个跨进程使用的 DEMO,主进程向 30001 端口上的 gRPC 服务器发送请求,子进程也向相同的服务器发送请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def send():
    channel = grpc.insecure_channel('localhost:30001')
    stub = message_pb2_grpc.GreeterStub(channel)
    response = stub.SayHello(message_pb2.HelloRequest(name='you'))
    print(f"Greeter client received 1: " + response.message)

def main():
    channel = grpc.insecure_channel('localhost:30001')
    stub = message_pb2_grpc.GreeterStub(channel)
    response = stub.SayHello2(message_pb2.HelloRequest(name='you'))
    print("Greeter client received 2: " + response.message)
    p = multiprocessing.Process(target=send)
    p.start()
    p.join()

if __name__ == '__main__':
    main()

使用 gRPC.io 1.28.1 的情况下,会发生报错,主进程可以正常收到服务器的返回,但是子进程报 Socket operation on non-socket

1
2
3
4
5
6
raise _InactiveRpcError(state)
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNAVAILABLE
details = "Socket operation on non-socket"
debug_error_string = "{"created":"@1587481625.192071231","description":"Error received from peer ipv6:[::1]:50051","file":"src/core/lib/surface/call.cc","file_line":1056,"grpc_message":"Socket operation on non-socket","grpc_status":14}"
>

排查过程

根据代码,主进程和子进程分别创建了自己的 Channel,看上去逻辑没什么问题,没有什么思路,所以多尝试几种情况先测试一下吧。首先尝试了一下主进程和子进程请求不同的server,在 30001 和 30002 端口分别启动两个 gRPC Server,然后将客户端代码改为主进程请求 30001 端口,子进程请求 30002 端口,代码可以正常运行。测试到这里就更摸不着头脑了,代码明明写的是主进程子进程分别创建 Channel,现在的现象看上去像是在请求相同服务器的情况下,子进程复用了主进程的socket连接。gRPC 底层使用的是 HTTP2,而 HTTP2 使用了长连接,会不会是这个原因?

有了新的分帧机制后,HTTP/2 不再依赖多个 TCP 连接去并行复用数据流;每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别设定优先级。 因此,所有 HTTP/2 连接都是永久的,而且仅需要每个来源一个连接,随之带来诸多性能优势。 —— HTTP/2 简介

从 HTTP2 原理上来看还是说的过去的,恰好 gRPC 项目中有 Issue 提到了跨进程使用的问题,参见 Failed to run grpc python on multiprocessing #18321,开发者在其中说明了像 Demo 那样使用报错的原因。

gRPC Core’s API for fork support
A process may fork after invoking grpc_init() and use gRPC in the child if and only if the child process first destroys all gRPC resources inherited from the parent process and invokes grpc_shutdown().
Subsequent to this, the child will be able to re-initialize and use gRPC. After fork, the parent process will be able to continue to use existing gRPC resources such as channels and calls without interference
from the child process.

gRPC Python behavior at fork()
To facilitate gRPC Python applications meeting the above constraints, gRPC Python will automatically destroy and shutdown all gRPC Core resources in the child’s post-fork handler, including cancelling in-flight calls. From the client’s perspective, the child process is now free to create new channels and use gRPC.

简化的说,在 gRPC Core API 的层面,子进程使用 gRPC 需要先销毁掉从父进程 fork 过来的 gRPC 资源,重新创建连接才可以正常使用,否则可能陷入死锁。

同时,gRPC 对于 fork 行为的支持也有一个专门的文档。https://github.com/grpc/grpc/blob/master/doc/fork_support.md

The background Python thread was removed entirely. This allows forking after creating a channel. However, the channel must not have issued any RPCs prior to the fork. Attempting to fork with an active channel that has been used can result in deadlocks/corrupted wire data.

从文档和 Issue 的描述看,当主进程有活动状态的 gRPC 连接时,是不可以 fork 的,会引发死锁或者报错(可能和 HTTP2 的长连接机制有关系),如果要 fork,需要先关闭掉活动的连接,在 fork 出的子进程中重新建立 gRPC 连接(也就是主子进程各自持有各自的 HTTP2 连接)。

实践方案

综合文档和开发者在 Issue 中提到的方法,要想让 Demo 可以运行有如下三种方法。

1
2
3
4
5
6
7
8
9
10
def main():
# 使用 with 语句
    with grpc.insecure_channel('localhost:30001'as channel:
        stub = message_pb2_grpc.GreeterStub(channel)
        response = stub.SayHello2(message_pb2.HelloRequest(name='you'))
        print("Greeter client received 2: " + response.message)
   
p = multiprocessing.Process(target=send)
    p.start()
    p.join()

参考资料

https://grpc.github.io/grpc/python/grpc.html#channel-object

https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn

https://github.com/grpc/grpc/issues/18321

https://github.com/grpc/grpc/pull/16264

https://github.com/grpc/grpc/blob/master/doc/fork_support.md#111

https://grpc.github.io/grpc/python/grpc.html#grpc.Channel.close

探究vscode debug流程,解决无法运行go程序的问题

问题描述

vscode 无法以 run 模式运行 go 项目(只能以 debug 模式调试),并且有如下报错。

图中被遮盖的部分是项目内的 package,并非第三方 package,也就是说在以 run 模式运行 go 项目时无法找到其他的 go 文件,只能找到入口文件。

初步排查

找不到其他文件,首先想到的是 GO_PATH 的问题,但是项目使用了 go mod,允许在 GO_PATH 之外的路径创建项目,所以这个怀疑点排除。接下来怀疑 vscode 的配置有问题,每个 vscode 项目中都有 .launch.json 文件,配置运行代码时的环境,下面是项目中的 .launch.json。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version""0.2.0",
    "configurations": [
        {
            "name""Launch",
            "type""go",
            "request""launch",
            "mode""auto",
            "program""${workspaceRoot}/src/main.go",
            "env": {},
            "args": []
        }
    ]
}

可以看到 .launch.json 里没有指定程序的工作目录,debug 模式和 run 模式会不会默认的工作路径不同呢?于是在 main 函数里使用 os.Getwd() 打印一下当前的路径,结果如下:

  • debug 模式:项目所在目录
  • run 模式:用户目录

基本可以确认,run 模式下的工作路径设置不正确,导致找不到路径。在 .launch.json 中加入 cwd 参数,手动填入项目路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version""0.2.0",
    "configurations": [
        {
            "name""Launch",
            "type""go",
            "request""launch",
            "mode""auto",
            "program""${workspaceRoot}/src/main.go",
            "cwd""${workspaceRoot}",
            "env": {},
            "args": []
        }
    ]
}

但是修改 .launch.json 后运行程序,输出的工作目录仍然是用户目录,cwd 参数并没有生效。

探究 vscode 的 debug 流程

至此,bug 的气息越来越浓厚,cwd 参数没有生效,肯定有问题!

一不做二不休,索性看看 vscode 的调试流程吧,用一个很暴力的方式,看看点击运行按钮后,vscode 到底是如何运行 go 程序的。

1
2
3
4
5
package main
import "time"
func main() {
    time.Sleep(10000000000)
}

运行程序后,使用 ps -ef|grep go 查看进程。

截图中三个进程从上到下均是父子关系,也就是说在 vscode 中即便使用 run 模式运行,也不是直接执行 go run xxxx.go,这与 Goland 等其他 IDE 的行为是不同的。vscode 首先调用了 language server 中的 node,执行了 go extention(vscode 的 go 扩展,安装后才支持 go 语言项目)中的一个 goDebug.js,而后 goDebug.js 中调用了 go run xxxx.go。(图中 /tmp 路径下的 main 文件是 go run 执行过程中生成的二进制文件)

接下来查看 goDebug.js 的逻辑,找到了调用 go run 的代码。

1
this.debugProcess = spawn(getBinPathWithPreferredGopath('go', []), runArgs, { env });

查看代码上面几行的逻辑,根据参数的命名,可以猜测出来,.launch.json 中的配置在这里是可以获取到的。接下来直接修改 js 文件,进行调试,证实上述的猜测,由于我们无法直接看到 node goDebug.js 的输出,所以通过写入文件的方式进行调试。

1
fs.writeFile('test.log', this.debugProcess.cwd(), function (err) {}

加入这句后再次运行,我们可以看到 test.log 文件中已经打印出了这个进程的工作路径,也就是 go run 的工作路径,是用户目录。至此,可以将问题缩小到:在 node 调用 go run 时没有将 .launch.json 文件中的 cwd 传给子进程(go run)。

spawn 是 nodejs 中的函数,看一下 spawn 的文档可以发现,spawn 有三个参数 child_process.spawn(command[, args][, options]) 第三个参数 options 中可以指定 cwd 工作路径。而 goDebug.js 这段启动子进程的代码并没有设置 cwd,只设置了env 参数,这就是 run 模式无法运行 go 程序的原因。

解决方案

在发现这个问题时,vscode go extention的最新版本是0.13,这个问题暂时只能通过修改 goDebug.js 的源码解决,如下图所示加入注释中的代码,将 cwd 参数传入子进程,就可以解决问题。

同时,这个 bug 已经被解决,可以参考 ISSUE #3096,程序员在解决另一个问题这个 ISSUE 的问题时,“顺手”把 cwd 的问题修复了。在 vscode go extention 0.14版发布后(已发布),将 go extension 更新到最新版就可以正常运行和调试 go 项目了。

参考资料

Debugging in Visual Studio Code

Node.js v13.13.0 Documentation

Debug: add “go run .” support #3096

viper从etcd读取配置失败的问题

问题描述

Viper (本文环境是Viper 1.1.0)是Go应用程序的完整配置解决方案,在很多项目中都有应用。etcd是一个分布式KV存储,最直接的应用是配置中心。

Viper除了支持从文件中读取配置,还支持从远程的配置中心读取配置,使用下面的代码进行配置。

1
2
3
4
5
6
7
8
viper.AddRemoteProvider("etcd",
        "http://127.0.0.1:2379",
        "conf.toml")
viper.SetConfigType("toml")
err := viper.ReadRemoteConfig()
if err != nil {
panic(err)
}

运行后报错panic: Remote Configurations Error: No Files Found,检查后发现etcd开启了tls,所以需要用https协议访问etcd的API,更新代码如下。

1
2
3
4
5
6
7
8
9
viper.AddSecureRemoteProvider("etcd",
        "https://127.0.0.1:2379",
        "conf.toml",
"key_path")
viper.SetConfigType("toml")
err := viper.ReadRemoteConfig()
if err != nil {
panic(err)
}

使用AddSecureRemoteProvider方法替换AddRemoteProvider方法,问题依旧。

定位问题

跟踪源码发现,最终像etcd发送请求的是go-etcd包(目前go-etcd已经不维护),在go-etcd的requests.go文件中找到了相关的源码,go-etcd调用了net/http包向etcd发送请求。

这个时候忽然想到etcd的证书是自签名的,访问自签名证书的https接口应该会报错啊,怎么会请求到内容呢?如下图,在Chrome中访问etcd的自签名https接口,会提示证书无效。

我们自己使用go实现一段请求etcd https接口的代码,看看到底是什么回事,代码如下。

1
2
3
4
5
6
7
8
9
10
11
resp, err := http.Get("https://127.0.0.1:2379/v2/keys/conf.toml?quorum=false&recursive=false&sorted=false")
    if err != nil {
        // handle error
        fmt.Println(err)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(body))

果然不一样,我们的代码会报错x509: certificate signed by unknown authority,因为是自签名证书,客户端无法验证证书真假,所以这个报错是可以理解的,go-etcd代码和我们的测试代码表现不一致,一定是我们落下了什么,重新梳理go-etcd源码终于发现了原因。

在client.go文件的initHTTPSClient方法中,发现允许绕过证书验证,这就可以解释为什么证书无效也能获取到数据了,绕过了证书的验证环节,相当于不管证书真假都拿来用。现在可以解释使用AddRemoteProvider方法访问https接口为什么可以获取到内容,但是无法解释AddSecureRemoteProvider方法为什么无法从https接口获取内容,因为两个方法在发送http请求阶段的代码是一致的,都忽略了证书验证。

查看AddSecureRemoteProvider的注释,发现了原因。

原来…AddSecureRemoteProvider这个Secure指的并不是使用安全链接https,而是在请求到内容后加了一个解密的步骤(Secure指请求的是加密过的内容,而不是使用加密链接请求),最后一个参数接收的也并不是客户端证书,而是解密的gpg key… 根据viper的文档,这个gpg key是可选的,我们这个例子中,如果给gpg key传入一个空字符串,也是可以正常执行的…

必须吐槽一下viper的命名,哪里是AddSecureRemoteProvider,明明应该叫AddEncryptedRemoteProvider

总结

出现这个问题,主要是误会了AddSecureRemoteProvider接口表达的意思,并且go-etcd允许忽略证书验证,也让问题变得更加离奇。

当然go-etcd的这种配置是非常合理的,内部系统使用自签名证书是一个很正常的行为。

使用Pipfile代替reqirements.txt

很多语言都提供了环境隔离的支持,例如nodejs的node_module,golang的go mod,python也有virtualenv和pyvenv等机制。为了建立依赖快照,通常会用pip freeze > requirements.txt 命令生成一个requirements.txt文件,在一些场景下这种方式就可以满足需求,但是在复杂场景下requirements.txt就力不从心了。

requirements.txt

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
appdirs==1.4.3
astroid==2.3.3
attrs==19.3.0
black==19.3b0
certifi==2019.11.28
chardet==3.0.4
click==7.1.1
et-xmlfile==1.0.1
Flask==1.1.1
gevent==1.4.0
greenlet==0.4.15
idna==2.9
isort==4.3.21
itsdangerous==1.1.0
jdcal==1.4.1
Jinja2==2.11.1
lazy-object-proxy==1.4.3
MarkupSafe==1.1.1
mccabe==0.6.1
numpy==1.18.2
openpyxl==3.0.3
pandas==1.0.3
pylint==2.4.4
python-dateutil==2.8.1
pytz==2019.3
requests==2.23.0
six==1.14.0
tinydb==3.15.2
toml==0.10.0
typed-ast==1.4.1
urllib3==1.25.8
Werkzeug==1.0.0
wrapt==1.11.2

requirements.txt文件中只记录了依赖的版本,所以如果遇到官方的pypi源下载速度慢,需要使用更快的国内镜像下载,通常只能使用pip install -i安装或者修改全局的pip.conf文件。

当某个项目使用确定的python版本,这个版本也并不能在requirements.txt中体现,只能通过readme或者文档来记录,并且需要在创建虚拟环境时手动调用正确的python版本。

项目需要使用flake8、pylint、black等代码优化工具时,这些依赖也会被pip freeze命令写入requirements.txt中,然而这些依赖是不需要出现在生产环境的。

Pipfile

Pipenv的出现,一举解决了上面的问题,Pipenv是Kenneth Reitz在2017年1月发布的Python依赖管理工具,他所基于的Pipfile则用来替代requirements.txt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[[source]]
name = "pypi"
url = "https://pypi.doubanio.com/simple"
verify_ssl = false

[dev-packages]
isort = "*"
black = "==19.3b0"
pylint = "*"

[packages]
flask = "*"
tinydb = "*"
pandas = "*"
requests = "*"
gevent = "*"
openpyxl = "*"

[requires]
python_version = "3.6"

好处1:记录内容更详细

相比于requirements.txt,Pipfile多了pip源的设置,可以针对不同项目使用不同环境。并且将依赖分为dev和默认环境,例如pylint、flake8、black等依赖,可以将他们放入dev依赖中。

好处2:减少手动激活虚拟环境次数

pipenv将virtualenv、pyvenv和pip命令整合使用,pipenv减少了手动激活虚拟环境的次数,使用pyvenv模块运行main.py,需要先执行source venv/bin/activate激活虚拟环境,然后再执行python main.py,而pipenv只需要在项目根目录执行pipenv run main.py ,就可以自动激活当前虚拟环境并执行main.py。如果需要安装依赖的,直接执行pipenv install xxx,也不需要先激活虚拟环境,再使用pip install xxx安装。

好处3:锁机制

从Pipfile文件添加或删除安装的包,会生成Pipfile.lock来锁定安装包的版本和依赖信息,通过pipfile.lock文件,可以精确恢复以来的版本。

常用命令

1
2
3
4
5
6
7
8
9
10
11
# 初始化虚拟环境(可自己指定python版本)
$ pipenv --python 3.6.9

# 激活当前项目虚拟环境
$ pipenv shell

# 安装开发依赖包
$ pipenv install black --dev

# 生成lock文件
$ pipenv lock

Docker COPY 复制文件夹的诡异行为

问题现象

在制作 docker 镜像时,有复制某一个路径下所有文件和文件夹到镜像的需求,写下了如下 dockerfile:

1
2
3
FROM alpine
WORKDIR /root/test_docker_proj
COPY * ./

原始目录结构是这样的:

1
2
3
4
5
6
7
/projects/test_docker_proj
├── Dockerfile
├── dir1
│   ├── dir11
│   │   └── file11
│   └── file1
└── file2

然而复制到 docker 镜像里的目录结构变成了这样:

1
2
3
4
5
6
/root/test_docker_proj
├── Dockerfile
├── dir11
│   └── file11
├── file1
└── file2

探究 Pandas 读取 Excel 文件报错问题

问题描述

使用 Pandas 的 read_excel 方法读取一个 16 万行的 Excel 文件报 AssertionError 错误:

1
2
3
  "/Users/XXX/excel_test/venv/lib/python3.7/site-packages/xlrd/xlsx.py", line 637, in do_row
assert 0 <= self.rowx < X12_MAX_ROWS
AssertionError

使用代理进行 docker build 问题的解决思路

问题描述

在使用 docker build 打包镜像时,遇到了需要使用代理访问网络的需求。使用如下的 Dockerfile 来模拟这个场景:

1
2
FROM golang:1.12
RUN curl www.google.com --max-time 3

国内一般网络环境下,curl www.google.com 是无法正常返回的,加入 –max-time 让 curl 的耗时不要太长。