分类 默认分类 下的文章

诊断 Python 占用一个 CPU 的问题

早上刚打开 MAC 笔记本 5分钟, 就提示我电池快没电了, 于是感觉给它供电. 紧接着, 就听到风扇呼呼作响, 于是查看 Activity Monitor, 发现有个 Python 进程几乎占用一个CPU, 感觉有 bug, 也许是项目的那个代码没写好. hmm, 生产环境不会也是这样吧?

Activity_Monitor.png

先确定进程信息

有了进程号, 进程很容易确定.

% ps aux  | grep 24315
xiatian          24315  99.3  0.0 36938856   9804   ??  R    Mon10PM 1044:13.73 /usr/local/Cellar/python@3.11/3.11.11/Frameworks/Python.framework/Versions/3.11/Resources/Python.app/Contents/MacOS/Python /Users/supra/work/projects/agent-ui/main.py

再做 CPU profiling

py-spy 是一个非常流行的采样 profiler,可以在不修改代码的情况下分析任何正在运行的 Python 程序。

pip install py-spy
py-spy record -o profile.svg --pid 24315

火焰图如下:
profile_svg.png

前面三行都是 Python 自带 python3.11/threading.py 里面的代码. 最后一行是 concurrent/futures/thread.py) 的代码, 代码如下:

    try:
        while True:
            work_item = work_queue.get(block=True)
            if work_item is not None:
                work_item.run()
                # Delete references to object. See issue16284
                del work_item

                # attempt to increment idle count
                executor = executor_reference()
                if executor is not None:
                    executor._idle_semaphore.release()
                del executor
                continue

            executor = executor_reference()
            if _shutdown or executor is None or executor._shutdown:
                # Flag the executor as shutting down as early as possible if it
                # is not gc-ed yet.
                if executor is not None:
                    executor._shutdown = True
                # Notice other workers
                work_queue.put(None)
                return
            del executor
    except BaseException:
        _base.LOGGER.critical('Exception in worker', exc_info=True)

看上去从 work_queue.get(block=True) 拿 item , 每次拿到的都是 None. 那么下一个问题就是这个 work_queue 从哪里来的, 为什么会以极快的速度返回一个 None?

交叉验证

要回答上面的问题, 就要深入内存去看这个 work_queue 从哪里来的, 它引用了那些对象, 哪些对象还在引用它. 这样才能确定这个 work_queue 有什么问题. 但是, 在此之前, 我们可以去production 看一下, 看看在prod 的 Linux 上是不是有类似的问题, 结果发现完全没问题, 并且它运行的时间更长.

于是猜测, 这个问题难道只是个 MAC 有关? 或者和 MAC 休眠有关?

python Async Sync 函数

这篇是比较好的一篇文章: https://docs.chainlit.io/guides/sync-async

  1. sync -> sync -> ok
  2. sync -> async -> asyncio.run() | asyncio.get_event_loop().run_until_complete(async_func())
  3. async -> sync -> ok | await asyncio.to_thread(sync_task)
  4. async -> async -> await
  5. Python 的 asyncio 库是标准库中用于编写异步 I/O 代码的核心框架,自 Python 3.4 引入后成为构建高性能网络应用和并发程序的重要工具. asyncio 是 Python 异步编程的基石,适合需要高效处理 I/O 并发的场景。通过事件循环和协程机制,开发者能以同步风格编写异步代码,显著提升程序性能.
  6. Asyncer 是一个用于简化 Python 异步编程的第三方库,它通过更优雅的语法和高级功能让异步代码更易编写和维护. 相比标准库 asyncio,Asyncer 通过更高层次的抽象降低了异步编程门槛,适合需要快速开发高并发应用的场景.
  7. Python 的 anyio 库是一个​​跨异步框架的统一接口库​​,旨在简化异步编程的复杂性,同时兼容主流异步模型(如 asyncio 和 trio)。

证书的生成和验证

1. 安装 OpenSSL

首先,确保您的系统上安装了 OpenSSL。在大多数 Linux 发行版中,您可以使用包管理器来安装它。

例如,在 Ubuntu 上:

sudo apt update
sudo apt install openssl

2. 生成私钥

私钥是安全证书的核心,应该保密。使用以下命令生成私钥:

openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:2048

这里,-algorithm RSA 指定了使用 RSA 算法,-out private.key 指定了输出文件名,rsa_keygen_bits:2048 设置了密钥长度。

3. 生成 CSR(证书签名请求)

CSR 是发送给证书颁发机构以获取签名证书的请求。生成 CSR 时,系统会提示您输入一些信息,如国家、州、组织等。

openssl req -new -key private.key -out certificate.csr

4. 生成自签名证书

在测试环境中,您可以使用 OpenSSL 生成自签名证书。以下是命令:

openssl x509 -req -days 365 -in certificate.csr -signkey private.key -out certificate.crt

这里,-days 365 设置了证书的有效期限为一年,-in certificate.csr 指定了 CSR 文件,-signkey private.key 指定了用于签名的私钥,-out certificate.crt 指定了输出证书文件。

5. 查看证书信息

您可以使用以下命令来查看证书的内容:

openssl x509 -in certificate.crt -text -noout

可以看到之前的设置内容:

 Signature Algorithm: sha256WithRSAEncryption
Issuer: C=CN, ST=SH, L=Shanghai, O=tianxiaohui.com, CN=10.236.90.154, emailAddress=eric@txh.com
Subject: C=CN, ST=SH, L=Shanghai, O=tianxiaohui.com, CN=10.236.90.154, emailAddress=eric@txh.com

6. 使用证书

生成的 certificate.crt(证书)和 private.key(私钥)可以用于配置 HTTPS 服务器,如 Apache 或 Nginx。

安装证书到 Flask server app

一个基于 Flask 到本地app:

from flask import Flask

app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
def index():
    return 'Hello, World!'
# ... 你的应用代码 ...


if __name__ == '__main__':
    # 指定证书和私钥的路径
    app.run(host='0.0.0.0', port=443, ssl_context=('certificate.crt', 'private.key'))

本地访问

本地浏览器访问的到下面的错误:
Your connection is not private
使用 python requests 访问:

requests.exceptions.SSLError: HTTPSConnectionPool(host='10.236.90.154', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1129)')))

非对称加密 RSA

使用 openssl 命令行工具生成RSA公钥和私钥

这个命令将生成一个2048位的RSA私钥,并将其保存在文件 private.pem 中

openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048

以下命令从私钥中提取公钥(public.pem):

openssl rsa -pubout -in private.pem -out public.pem

为什么可以从私钥中提取公钥?

钥文件通常包含了生成密钥对所需的所有信息,包括p、q、n、e和d。因此,可以从私钥文件中提取出n和e,这两者就构成了公钥

使用 python 生成 RSA 公钥和私钥

首先安装 cryptography 包.

 pip install cryptography

以下代码使用 cryptography 来生成 RSA 公钥和私钥.

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# 生成私钥
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)

# 将私钥序列化为PEM格式
pem_private_key = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

# 从私钥中获取公钥
public_key = private_key.public_key()

# 将公钥序列化为PEM格式
pem_public_key = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# 打印私钥和公钥
print("Private Key (PEM format):")
print(pem_private_key.decode('utf-8'))

print("\nPublic Key (PEM format):")
print(pem_public_key.decode('utf-8'))

# 你可以将私钥和公钥保存到文件中
with open('private_key.pem', 'wb') as f:
    f.write(pem_private_key)

with open('public_key.pem', 'wb') as f:
    f.write(pem_public_key)

加密和解密

公钥加密消息:

# message.txt 是要加密的原始消息文件,encrypted_message.bin 是加密后的输出文件
openssl rsautl -encrypt -in message.txt -inkey public.pem -pubin -out encrypted_message.bin

私钥解密消息:

# encrypted_message.bin 是加密后的消息文件,decrypted_message.txt 是解密后的输出文件
openssl rsautl -decrypt -in encrypted_message.bin -inkey private.pem -out decrypted_message.txt

签名和验证签名

签名涉及2步, 第一步对消息做哈希,生成 hash值, 第二步对 hash 值做加密(即签名).

python 版本签名.

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key

# 假设你已经有了私钥,这里我们使用之前生成的私钥
# load your private key from local file: private_key.pem
with open('private_key.pem', 'rb') as file:
    pem_private_key = file.read()

# 加载私钥
private_key = load_pem_private_key(pem_private_key, password=None, backend=default_backend())

# 要签名的消息
message = b"this is a message from https://www.tianxiaohui.com"

# 生成消息的哈希值
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(message)
digest_value = digest.finalize()

# 使用私钥对哈希值进行签名
signature = private_key.sign(
    digest_value,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    hashes.SHA256()
)

# 打印签名
print("Signature:", signature)

# 使用公钥验证签名
# public_key = private_key.public_key()
with open('public_key.pem', 'rb') as file:
    public_key = load_pem_public_key(file.read())

try:
    public_key.verify(
        signature,
        digest_value,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print("Signature is valid.")
except:
    print("Signature is invalid.")

openssl 版本

# 生成RSA密钥对
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

# 创建一个文件进行签名
echo "This is a test message." > your_file.txt

# 使用私钥进行签名
openssl dgst -sha256 -sign private_key.pem -out signature.sig your_file.txt
# -sha256 表示使用SHA-256哈希算法。
# -sign private_key.pem 指定私钥文件。
# -out signature.sig 指定输出签名文件的名称。
# your_file.txt 是你要签名的文件。

# 使用公钥验证签名
openssl dgst -sha256 -verify public_key.pem -signature signature.sig your_file.txt

# -sha256 表示使用SHA-256哈希算法。
# -verify public_key.pem 指定公钥文件。
# -signature signature.sig 指定签名文件。
# your_file.txt 是你之前签名的文件。

公钥和私钥的格式

公钥和私钥可以以多种不同的格式保存,这些格式主要分为两大类:非加密格式和加密格式。以下是一些常见的密钥保存格式:

非加密格式

  1. PEM (Privacy Enhanced Mail) 格式:

    • 以文本形式存储,可以包含Base64编码的私钥或公钥。
    • 通常以 -----BEGIN PUBLIC KEY----------END PUBLIC KEY----------BEGIN PRIVATE KEY----------END PRIVATE KEY----- 这样的标记开头和结尾。
  2. DER (Distinguished Encoding Rules) 格式:

    • 二进制格式,通常用于存储证书和密钥。
    • 不以文本形式存储,因此不能直接编辑。
  3. PKCS#1 格式:

    • 主要用于RSA密钥。
    • 可以是PEM编码的文本格式,也可以是DER格式的二进制格式。

加密格式

  1. PKCS#8 格式:

    • 专门用于私钥的存储,可以包含加密的私钥。
    • 可以是PEM编码的文本格式,也可以是DER格式的二进制格式。
    • 如果加密,通常会有 -----BEGIN ENCRYPTED PRIVATE KEY----------END ENCRYPTED PRIVATE KEY----- 标记。
  2. PKCS#12 (PFX) 格式:

    • 用于存储私钥、公钥和证书,通常包含一个密码保护的档案。
    • .pfx.p12 为文件扩展名。
    • 支持二进制格式,可以包含多个密钥和证书。

其他格式

  1. SSH 格式:

    • 专门用于SSH密钥。
    • 私钥通常以 .ssh/id_rsa 文件形式存在,公钥以 .ssh/id_rsa.pub 文件形式存在。
    • 私钥文件通常是PEM格式,但公钥文件是SSH专用的格式。
  2. XML 格式:

    • 少见,但某些应用程序可能会使用XML格式来存储密钥。

Python urllib self-signed certificate in certificate chain

从上周开始, 本地一个 Python 的项目里面一只报下面的错:

requests.exceptions.SSLError: (MaxRetryError("HTTPSConnectionPool(host='huggingface.co', port=443): Max retries exceeded with url: /bert-base-uncased/resolve/main/tokenizer_config.json (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1006)')))"), '(Request ID: 6d1db4c8-5589-4dfd-a14f-b95e47b9864d)')

从出错栈看, 当连接任何 https 网站的时候, 都会报这个错. 即便使用 pip install 的时候, 一样报这个错. 所以, 是本地连接任何 SSL 站点时候, 验证证书出错了. 由于连接任何 https 网站都有问题, 所以是本地的问题, 而不是各个 https 网站的问题.

Google 了一圈, 也没有找到解决方案. 但是有人提供了 truststore 这个包, 可以把当前操作系统的所有证书替换 Python 环境的, 如下代码, 可以暂时解决这个问题.

import truststore
truststore.inject_into_ssl()

后来发现, 其实使用 requests 包是不会出错的, 正常连接并返回. 可是某些不使用 requests 包的请求会出错.

import requests
print(requests.get("http://www.google.com")) # 一切正常

网上继续查, 很多人用下面的代码去验证使用的哪些 CA:

import certifi; print(certifi.where())

import _ssl; print(_ssl.get_default_verify_paths())

因为 Python 默认使用 openssl 的CA, 通过上面的 _ssl.get_default_verify_paths() 可以获得. 安装 certifi 因为它带了权威的 CA. 所以一般在安装完 Python 之后, 会有个脚本把 openssl 的 CA 指向 certifi 再带的证书. 详情: https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error

我这个问题, 即便把证书指向 certifi, 或者使用 certifi 生成 SSLContext 依然不能解决. 虽然不能解决, 但是发现它其实有个共同点, 就是都是用了 Python 自带的 urllib 来连接. 使用 requests 包来连接的都没问题.

看 urllib 有关这个问题的讨论, 发现有些人使用了 VPN 或者 代理, 但是我机器或代码并没有设置代理.

最终发现公司在所有人电脑上装了 Netskope, 这个东西相当于一个代理, 所有本地网络都会走它, 并且公司的软件中心有个 Netskope Certificates Apply Fix.

安装完这个之后, 我只想说 TMD. 耽误我8个多小时.

安装背后

其实 Netskope 重新整理了一个bundled 证书, 放在了 /Library/Application\ Support/Netskope/STAgent/download/nscacert_combined.pem . 只要把上面2个 python 使用的证书换成 Netskope 重新整理的证书就好了.

后续

后面发现事情的真相.

通常情况下, 我在公司的电脑访问 https://www.google.com 的证书是由 WR2 (Google Trust Services) 签发的.
但是, 由于安装了 Netskope, 那么它拦截了所有出入的请求, 现在访问 https://www.google.com, Google 的证书是由公司的根证书的二级证书签发的, 所以本地不包含公司CA的 CA 列表是无法认识这个由公司签发的证书的.