Elasticsearch Python 客户端是否与自由线程 Python 兼容?

123 阅读6分钟

作者:来自 Elastic Quentin_Pradet

在这篇文章中,我们将进行一些实验,看看 Python Elasticsearch 客户端是否与新的 Python 3.13 自由线程(free-threading)版本兼容,其中 GIL 已被删除。

介绍

但首先,什么是 GIL?全局解释器锁 (Global Interpreter Lock - GIL) 是一个保护对 Python 对象访问的互斥锁,可防止多个线程同时执行 Python 字节码。在实践中这并不总是一个问题。

  • 科学编程可以使用不包含 GIL 的库(如 NumPy)。
  • 有些程序不是 CPU 密集型的,而是 I/O 密集型的。例如,如果你的代码向 Elasticsearch 发出昂贵的请求,但不会对结果进行昂贵的计算,则它可以有效地使用多个线程。事实上,即使只有一个线程正在执行,它也不会阻塞等待 I/O 的其他线程,从而不会阻塞 GIL。 (这也是 async/await 在 Python 中大放异彩的场景。)

然而,几十年来,人们的目标一直是消除这一限制并实现真正的多线程编程。感谢 Sam Gross 的出色工作,现在这一切成为了可能!这项工作最初被称为 nog​​il,但现在被称为 free-threading。虽然现有的纯 Python 代码与生成的构建仍然以相同的方式工作(尽管目前单线程代码速度较慢),但从 C 或 Rust 等其他语言编译的所有代码都需要重构。在过去,这种向后不兼容的变化足以成为发布 Python 4 的理由。然而,Python 3 迁移导致了超过 10 年的语言分裂,由此造成的痛苦仍然历历在目。因此,目前的计划是逐步推出

  • 作为第 1 阶段(当前阶段)的一部分,Python 3.13 提供了实验性的自由线程版本,每个库和应用程序都需要测试它们的兼容性。
  • 在第二阶段,这些构建将不再被称为 “实验性的”。
  • 在第 3 阶段,标准 Python 构建将包括自由线程支持。

Elasticsearch Python 客户端是纯 Python 代码,不涉及太多线程或特定依赖垃圾收集器,因此它应该可以与自由线程构建一样好地运行。但是,它确实具有受影响的可选依赖项,例如 aiohttp 或 orjson。

我们将测试这些不同的部件,看看它们是否正常工作。基准测试将作为练习留给读者!

使用自由线程 Python

多种方法可以安装自由线程的 Python 版本。我们将使用 Astral 的 uv 包管理器,它允许使用 --python 3.13t 指定自由线程构建。 Astral 为 python-build-standalone 贡献了自由线程构建,如果需要,uv 将会使用它们:

`

1.  $ uv run --python 3.13t python
2.  Using CPython 3.13.0
3.  Creating virtual environment at: .venv
4.  Installed 4 packages in 16ms
5.  Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 08:24:33)
6.  [Clang 18.1.8 ] on darwin
7.  Type "help", "copyright", "credits" or "license" for more information.
8.  >>>

`代码解释

但是,如果你已经安装了自由线程解释器,uv 将使用它而不是 python-build-standalone。例如,如果你想在 macOS 上使用 Homebrew 提供的构建(使用 brew install python-freethreading 安装),你将得到以下输出:

`

1.  $ uv run --python 3.13t python
2.  Using CPython 3.13.0 interpreter at:
3.  /opt/homebrew/opt/python-freethreading/bin/python3.13t
4.  Creating virtual environment at: .venv
5.  Installed 4 packages in 4ms
6.  Python 3.13.0 experimental free-threading build (main, Oct  7 2024, 05:02:14)
7.  [Clang 16.0.0 (clang-1600.0.26.4)] on darwin
8.  Type "help", "copyright", "credits" or "license" for more information.
9.  >>>

`代码解释

由于 uv 还支持内联脚本元数据标准,我们将提供如下独立的代码片段:

`

1.  # /// script
2.  # requires-python = ">=3.13"
3.  # dependencies = [
4.  #     "numpy",
5.  # ]
6.  # ///
7.  import numpy as np

9.  c = np.arange(24).reshape(2, 3, 4)

`代码解释

你可以运行它们,而不必担心虚拟环境或手动安装依赖项:

``

1.  $ uv run --python 3.13t example.py
2.  Reading inline script metadata from `example.py`
3.  [[[ 0  1  2  3]
4.    [ 4  5  6  7]
5.    [ 8  9 10 11]]

7.   [[12 13 14 15]
8.    [16 17 18 19]
9.    [20 21 22 23]]]

``代码解释

使用 Elasticsearch

得益于 start-local 脚本,Elasticsearch 同样易于运行:

``

1.  $ curl -fsSL https://elastic.co/start-local | sh

3.    ______ _           _   _
4.   |  ____| |         | | (_)
5.   | |__  | | __ _ ___| |_ _  ___
6.   |  __| | |/ _` / __| __| |/ __|
7.   | |____| | (_| \__ \ |_| | (__
8.   |______|_|\__,_|___/\__|_|\___|
9.  -------------------------------------------------
10.  🚀 Run Elasticsearch and Kibana for local testing
11.  -------------------------------------------------

13.  ℹ️  Do not use this script in a production environment

15.  ⌛️ Setting up Elasticsearch and Kibana v8.16.0...

17.  - Generated random passwords
18.  - Created the elastic-start-local folder containing the files:
19.    - .env, with settings
20.    - docker-compose.yml, for Docker services
21.    - start/stop/uninstall commands
22.  - Running docker compose up --wait

24.  🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!

26.  🌐 Open your browser at http://localhost:5601
27.  🔌 Elasticsearch API endpoint: http://localhost:9200

``代码解释

我们来测试一下:

`

1.  # /// script
2.  # requires-python = ">=3.13"
3.  # dependencies = [
4.  #     "elasticsearch",
5.  # ]
6.  # ///
7.  import os
8.  import sys

10.  from elasticsearch import Elasticsearch

12.  print(sys.version)
13.  client = Elasticsearch(
14.      "http://localhost:9200", api_key=os.environ["ES_LOCAL_API_KEY"]
15.  )
16.  print(client.info()["tagline"])

`代码解释

虽然 start-local 不使用 HTTPS,但它确实设置了身份验证。相关机密存储在 elastic-start-local/.env 文件中,因此我们可以获取它并将 ES_LOCAL_API_KEY 作为环境变量传递:

``

1.  $ source elastic-start-local/.env
2.  $ ES_LOCAL_API_KEY=$ES_LOCAL_API_KEY uv run --python 3.13t ex1.py
3.  Reading inline script metadata from `ex1.py`
4.  3.13.0 experimental free-threading build (main, Oct 16 2024, 08:24:33)
5.  [Clang 18.1.8 ]
6.  You Know, for Search

``代码解释

太棒了!一个简单的查询就按预期工作了。现在,让我们测试 Python 客户端的其他区域。

批量助手 - bulk helper

我们在 Python 客户端中明确使用线程的唯一地方是在 parallel_bulk 帮助程序中。让我们索引 books.csv 数据集并进行查询以查看是否有效。

`

1.  # /// script
2.  # requires-python = ">=3.13"
3.  # dependencies = [
4.  #     "elasticsearch",
5.  # ]
6.  # ///
7.  import csv
8.  import os
9.  import sys
10.  import time

12.  from elasticsearch import Elasticsearch, helpers

14.  client = Elasticsearch(
15.      "http://localhost:9200", api_key=os.environ["ES_LOCAL_API_KEY"]
16.  )

18.  mappings = {
19.      "properties": {
20.          "Title": {"type": "text"},
21.          "Description": {"type": "text"},
22.          "Author": {"type": "text"},
23.          "Year": {"type": "date", "format": "yyyy"},
24.          "Published": {"type": "keyword"},
25.          "Rating": {"type": "scaled_float", "scaling_factor": 100},
26.      }
27.  }

29.  client.options(ignore_status=[404]).indices.delete(index="books")
30.  client.indices.create(index="books", mappings=mappings)
31.  print("Created index")

34.  def generate_docs():
35.      with open("books.csv", newline="") as csvfile:
36.          reader = csv.DictReader(csvfile, delimiter=";")
37.          for row in reader:
38.              yield {"_index": "books", **row}

41.  start = time.perf_counter()
42.  n, errors = helpers.bulk(client, generate_docs())
43.  end = time.perf_counter()
44.  print(f"Indexed {n} books in {end - start:.1f} seconds.")

46.  client.indices.refresh(index="books")

48.  print("Searching for Stephen King:")
49.  resp = client.search(
50.      index="books", query={"match": {"Author": "Stephen King"}}
51.  )
52.  for hit in resp.body["hits"]["hits"]:
53.      book = hit["_source"]
54.      description = f'{book["Author"]} - {book["Title"]} ({book["Year"]})'
55.      rating = f'{book["Ratings"]} stars'
56.      print(f"  {description}: {rating}")

`代码解释

脚本的输出显示我们确实在不到 2 秒的时间内索引了所有 82k 本书!这比标准批量助手快大约 2 倍

``

1.  $ ES_LOCAL_API_KEY=$ES_LOCAL_API_KEY uv run --python 3.13t ex2.py
2.  Reading inline script metadata from `ex2.py`
3.  Created index
4.  Indexed 81828 books in 1.6 seconds.
5.  Searching for Stephen King:
6.    Stephen King - THE ELEMENTS OF STYLE (2013): 5.00 stars
7.    Stephen King - Star (Thorndike Core) (2021): 3.11 stars
8.    Stephen King - Hearts in Atlantis (2017): 4.08 stars
9.    Stephen King - Misery (Spanish Edition) (2016): 4.43 stars
10.    Stephen King - The Dead Zone (2016): 4.40 stars
11.    Stephen King - Another World (Thorndike Core) (2021): 3.78 stars
12.    Stephen King - FROM A BUICK 8 (True first edition) (2017): 3.25 stars
13.    Stephen King - Road Work (2017): 4.29 stars
14.    Stephen King - Icon (Thorndike Core) (2021): 4.00 stars
15.    Stephen King - Misery (2016): 4.43 stars

``代码解释

aiohttp

Elasticsearch 的 Python 客户端通过两个 HTTP 客户端(aiohttphttpx)支持 asyncio,默认使用 aiohttp。虽然 aiohttp 尚未正式支持自由线程构建(目前确实无法编译),但可以通过设置 AIOHTTP_NO_EXTENSIONS=1 在纯 Python 模式下使用它。虽然性能会较慢,但可以与自由线程构建兼容。

关于测试,没有太多需要测试的内容,因为 asyncio 事件循环已经局限于单个线程。接下来,让我们复用之前的示例,但改用 asyncio:

`

1.  # /// script
2.  # requires-python = ">=3.13"
3.  # dependencies = [
4.  #     "elasticsearch[async]",
5.  # ]
6.  # ///
7.  import asyncio
8.  import os
9.  import sys

11.  from elasticsearch import AsyncElasticsearch

13.  print(sys.version)

16.  async def main():
17.      async with AsyncElasticsearch(
18.          "http://localhost:9200", api_key=os.environ["ES_LOCAL_API_KEY"]
19.      ) as client:
20.          info = await client.info()
21.          print(info["tagline"])

24.  asyncio.run(main())

`代码解释

由于 uv run 会动态安装依赖项,因此我们需要定义 AIOHTTP_NO_EXTENSIONS 来运行。事实上,脚本的行为符合预期:

``

1.  $ export AIOHTTP_NO_EXTENSIONS=1
2.  $ export ES_LOCAL_API_KEY=$ES_LOCAL_API_KEY
3.  $ uv run --python 3.13t ex3.py
4.  Reading inline script metadata from `ex3.py`
5.  3.13.0 experimental free-threading build (main, Oct 16 2024, 08:24:33
6.  [Clang 18.1.8 ]
7.  You Know, for Search

``代码解释

序列化和反序列化

Elasticsearch Python 客户端支持多个库来序列化或反序列化数据。出于性能原因,他们经常使用本机代码,并且这些库需要进行调整才能与自由线程构建配合使用。

orjson 允许快速序列化/反序列化 JSON,但尚不支持自由线程构建,甚至无法编译。

PyArrow 18+ 和 Pandas 2.2.3+ 支持自由线程构建。让我们通过进行 ES|QL 查询来重用书籍索引:

`

1.  # /// script
2.  # requires-python = ">=3.13"
3.  # dependencies = [
4.  #     "elasticsearch",
5.  #     "pandas",
6.  #     "pyarrow",
7.  # ]
8.  # ///
9.  import csv
10.  import os
11.  import sys
12.  import time

14.  import pandas as pd
15.  from elasticsearch import Elasticsearch, helpers

17.  client = Elasticsearch(
18.      "http://localhost:9200", api_key=os.environ["ES_LOCAL_API_KEY"]
19.  )

21.  print("Searching for Stephen King:")
22.  resp = client.esql.query(
23.      query="""
24.      FROM books
25.      | WHERE Author == "Stephen King"
26.      | SORT Rating DESC
27.      | LIMIT 10
28.      """,
29.      format="arrow",
30.  )
31.  df = resp.to_pandas(types_mapper=pd.ArrowDtype)
32.  print(df)

`代码解释

输出以下内容:

``

1.  $ PYTHON_GIL=0 ES_LOCAL_API_KEY=$ES_LOCAL_API_KEY uv run --python 3.13t ex4.py
2.  Reading inline script metadata from `ex4.py`
3.  Searching for Stephen King:
4.           Author  ...                                Title                 Year
5.  0  Stephen King  ...       Another World (Thorndike Core)  2021-01-01 00:00:00
6.  1  Stephen King  ...  FROM A BUICK 8 (True first edition)  2017-01-01 00:00:00
7.  2  Stephen King  ...                   Hearts in Atlantis  2017-01-01 00:00:00
8.  3  Stephen King  ...             Misery (Spanish Edition)  2016-01-01 00:00:00
9.  4  Stephen King  ...       The Dark Tower: The Gunslinger  2017-01-01 00:00:00
10.  5  Stephen King  ...                        The Dead Zone  2016-01-01 00:00:00
11.  6  Stephen King  ...           NIGHTMARES AND DREAMSCAPES  2017-01-01 00:00:00
12.  7  Stephen King  ...                    How writers write  2002-01-01 00:00:00
13.  8  Stephen King  ...                THE ELEMENTS OF STYLE  2013-01-01 00:00:00
14.  9  Stephen King  ...                            Road Work  2017-01-01 00:00:00

``代码解释

请注意,我必须设置 PYTHON_GIL=0 来禁用以下警告,我认为不应该发出该警告,因为这些库确实支持自由线程构建。也许这个问题将在未来的版本中得到修复。

结论

总而言之,自由线程构建的效果出奇地好!许多重要的库已经支持自由线程。虽然仍然存在一些不受支持的库,例如 orjson 或 Polars,但它们是例外,而不是规则。自由线程的前景光明,我可以看到这些构建很快就会脱离实验状态。 (但在这种情况发生之前,我建议不要在生产中使用它们。)

如果你想了解有关自由线程的更多信息,py-free-threading.github.io/是一个很好的资源,特别是更多资源页面链接到有用的学习材料。

回答我最初的问题:是的,Python Elasticsearch 客户端在自由线程下运行得很好!

原文:Dec 4th, 2024: [EN] Does the Elasticsearch Python client work with free-threading Python? - Advent Calendar - Discuss the Elastic Stack