2024年8月

有时候我们需要再同一台机器上创建多个数据库服务(不是单纯的数据库实例),每一个数据库可以有单独的服务运行,只是在一个机器环境而已。可以在不同的端口上监听,也可以在相同端口监听

创建多个数据库步骤

安装完Oracle数据库后,会自动安装很多工具,这里我们使用Database Configuration Assistant工具来创建数据库
image
按照提示一步步确定和填写信息即可,注意第三部填写自己要创建的数据库名称:
image
整个过程和第一次创建数据库一模一样,注意按需选择字符集、连接数等即可。
创建成功后,我们的呢服务列表services.msc中会自动运行刚才创建好的数据库服务,比如我这里刚才创建的名字叫looorcl:
image

连接指定数据库

  • 一般情况下,创建成功后,直接采用sqlplus去连接就行,只不过连接字符串改成上面刚新加的就行,比如:
    sqlplus sys/nhis@looOrcl as sysdba
    我这里是本地所以没有制定ip
  • 如果连接时报错:ERROR:ORA-12514: TNS: 监听程序当前无法识别连接描述符中请求的服务
  1. 此时要检查,首先是否书写错误。确保你在连接时使用的服务名与 tnsnames.ora 文件中定义的服务名一致。

  2. 检查tnsnames.ora这个文件的配置,具体路径在创建数据库的时候已经有提示,不改动的话默认就是数据库目录的
    product\11.2.0\dbhome_1\NETWORK\ADMIN
    目录中,比如我的在D:\app\Administrator\product\11.2.0\dbhome_1\NETWORK\ADMIN\tnsnames.ora,文件内容中会多出一个节点,比如:
    image

  3. 然后再查看 相同目录中的listener.ora文件,检查监听是否已经加入,也就是我们刚创建的数据库服务名称,是不是已经加入到文件,没有的话,可以手动填写,比如像我这样子:
    image

  4. 重启监听
    lsnrctl stop
    lsnrctl start
    启动成功后重新连接即可,这样等于是相同的端口,监听了两个不同的数据库。我们打开Net Manager可以看到,我们监听了两个出具库服务
    image
    image

创建不同监听端口的数据库服务

只需在上面的Net Manager中删除刚新建的数据库配置,然后新加监听,重新制定数据库名和SID,重新指定一个端口即可。

Python 项目及依赖管理工具,类似于 Java 中的 Maven 与 Node 中的 npm + webpack,在开发和维护项目时起着重要的作用。使用适当的依赖管理工具可以显著提高开发效率,减少依赖冲突,确保项目的稳定性、可靠性和安全性。

一、常见项目及依赖管理工具需具备的功能

1. 依赖管理

(1)自动化依赖安装

依赖管理工具可以自动安装项目所需的所有依赖包,而不需要手动逐个安装。

(2)依赖版本控制

这些工具允许开发者指定和锁定依赖包的版本,确保项目在不同环境中运行时依赖的一致性

2. 虚拟环境管理

虚拟环境允许在同一台机器上运行多个项目,而不会发生依赖冲突。依赖管理工具通常会自动创建和管理虚拟环境,确保项目依赖的隔离性。

3. 依赖冲突解决

依赖管理工具可以自动解决依赖冲突,确保安装的依赖包版本兼容。例如,
pipenv

poetry
都有内置的依赖冲突解决机制。

4. 安全性检查

一些工具(如
pipenv
)提供了内置的安全性检查功能,可以扫描依赖包的已知漏洞,并提供修复建议。

# 使用 pipenv 进行安全性检查
pipenv check

5. 项目初始化和模板

一些工具(如
poetry

hatch
)提供了项目模板和脚手架功能,帮助开发者快速创建新项目。

6. 发布依赖包

一些工具(如
poetry
)内置了包发布功能,可以方便地将项目发布到 PyPI(Python Package Index)。

# 使用 poetry 发布包
poetry publish --build

7. 生成依赖配置文件

部分依赖管理工具可以生成描述项目依赖的文件,方便团队协作和部署。例如,
pip-tools
可以生成
requirements.txt
文件。

8. 版本管理

这是指如何管理和控制你的项目版本。每次发布新版本时,你需要更新项目的版本号(如从 1.0.0 到 1.1.0),并确保版本号的变化遵循一定的规则(如语义化版本控制)。

版本管理工具可以帮助你自动更新版本号、生成变更日志、创建发布标签等。

二、常见工具及其技术对比

常见 Python 项目及依赖管理工具,包括
Pipenv

Poetry

Conda

Pip-tools

Hatch
)和
venv
。以下从工具简介、特性介绍、功能对比及流行度三方面进行对比。

1. 工具简介

(1)Pipenv

Pipenv
一度被官方推荐为 Python 项目的依赖管理工具,尤其是在需要自动管理虚拟环境和依赖锁定的场景中。然而,随着时间的推移,它的热度有所下降。

(2)Poetry

Poetry
近年来越来越流行,尤其在需要现代化依赖管理和包发布的项目中。它提供了更好的用户体验和更强大的功能。

(3)Conda
Conda
在数据科学和机器学习领域非常流行,因为它不仅支持 Python,还支持 R 和其他语言,并且其环境管理功能非常强大。

(4)Pip-tools

Pip-tools
在一些需要精确控制依赖版本的项目中很受欢迎,尤其是那些仍然使用
requirements.txt
的项目。

(5)Hatch

Hatch
是一个相对较新的工具,虽然功能强大,但目前还没有达到
Poetry

Conda
的流行度。

(6)venv

venv
是 Python 标准库中的模块,用于创建轻量级的虚拟环境,方便项目间的依赖隔离。

2. 特性介绍

工具 主要使用场景 主要特性
Pipenv 一般项目,自动管理虚拟环境 自动创建和管理虚拟环境、依赖锁定文件(Pipfile.lock)、友好的 CLI 接口
Poetry 现代化项目,依赖管理和包发布 全面的依赖管理、内置虚拟环境管理、项目构建和发布、依赖解析和锁定
Conda 数据科学和机器学习 跨语言支持、强大的环境管理、包管理、支持多种平台(Windows, macOS, Linux)
Pip-tools 精确控制依赖版本的项目 生成和更新 requirements 文件、依赖锁定、与 pip 兼容
Hatch 现代化项目管理和版本控制 项目模板、环境管理、版本控制、依赖管理、灵活的插件系统
venv 基本虚拟环境管理 Python 内置模块,轻量级虚拟环境管理

3. 功能对比

功能 venv Pipenv Poetry Conda Pip-tools Hatch
创建虚拟环境
自动管理虚拟环境
依赖文件
版本锁定
安全性检查
包发布
依赖冲突解决
项目模板和脚手架
版本管理

4. 流行度

当前(2024 年 7 月 30 日)各工具流行度,如下:

工具 GitHub Stars PyPI 近半年下载量(万) Forks Open Issues Open PRs
Pipenv 24k+ 6466 1862 260 14
Poetry 30k+ 20502 2236 602 74
Conda 6k+ 97 N/A N/A N/A
Pip-tools 7k+ 7254 608 160 27
Hatch 5k+ 1163 285 239 26
venv Python 内置 N/A N/A N/A N/A

三、结论

从上述技术对比来看,无论是功能特性还是流程程度,Poetry 都是当前最适合新 Python 项目的依赖管理工具。不过,部分其他工具也有一定适用场景,如 Pip-tools 适用于有 requirements 的老项目,Conda 适用于多编程语言项目。

四、VS pip

以 poetry 为例,简要介绍上述项目及包依赖管理工具与 pip 的区别。
pip
是一个轻量级的包管理工具,适合简单的包安装和管理任务。
poetry
是一个功能强大的项目管理工具,适合需要全面管理项目依赖和配置的场景。

1. 功能对比

特性 pip poetry
功能和用途 pip
是 Python 的包管理工具,用于安装和管理 Python 包。主要用于从 Python Package Index (PyPI) 下载和安装包,不涉及项目管理。
poetry
是一个全面的 Python 项目管理工具,不仅可以安装和管理包,还可以创建和管理项目、处理依赖关系、发布包等。
配置文件 pip
使用
requirements.txt
文件来列出项目的依赖包。
requirements.txt
是一个简单的文本文件,列出所有需要安装的包及其版本。
poetry
使用
pyproject.toml
文件来管理项目的元数据和依赖关系。
pyproject.toml
是一个结构化的配置文件,包含项目的详细信息、依赖关系、脚本等。
依赖管理 pip
本身不处理依赖冲突问题,需要开发者手动解决。可以结合
pip-tools
使用,以便更好地管理依赖关系。
poetry
内置依赖解析和锁定机制,可以自动解决依赖冲突问题。会生成一个
poetry.lock
文件,确保项目在不同环境下依赖一致。
虚拟环境管理 pip
本身不管理虚拟环境,但通常与
virtualenv

venv
一起使用。开发者需要手动创建和激活虚拟环境。
poetry
内置虚拟环境管理功能,可以自动创建和管理虚拟环境。使用
poetry
时,虚拟环境的创建和激活是自动处理的。

2. 使用示例

(1)pip

# 安装包
pip install requests

# 列出安装的包
pip freeze > requirements.txt

# 从 requirements.txt 安装包
pip install -r requirements.txt

(2)poetry

# 创建新项目
poetry new myproject

# 进入项目目录
cd myproject

# 安装包
poetry add requests

# 安装所有依赖
poetry install

# 启动虚拟环境
poetry shell

3. 建议

示例项目可用 Python 自带的 pip,简单易用。大型线上生产项目需要做好依赖管理、依赖冲突解决、虚拟环境管理等工作,以保证项目在多种环境下交付一致且稳定运行,poetry 之类的项目及包管理工具更合适的选择。

作者:来自 vivo 互联网服务器团队- Li Fan

本文从追溯时间轮算法的出现,介绍了时间轮算法未出现前,基于队列的定时任务实现,以及基于队列的定时任务实现所存在的缺陷。接着我们介绍了时间轮算法的算法思想及其数据结构,详细阐述了三种时间轮模型的数据结构和优劣性。

再次,我们介绍时间轮算法在 Dubbo 框架中的应用,并给出了它在 Dubbo 中的主要实现方式。

最后,我们以项目中的某个服务架构优化出发,介绍了目前设计中存在的缺陷,并借助来自中间件团队的,包含时间轮算法实现的延迟 MQ,给出了优化设计的方法。

第一章 定时任务及时间轮算法发展

1.1 时间轮算法的出现

在计算程序中,定时器用于指定一个具体的时间点去执行某一个既定的任务。而时间轮算法就是这样一种能够实现延迟功能(定时器)的巧妙算法。时间轮算法首次出现在 1997 年 George Varghese 和 Anthony Lauck 发表于 IEEE 期刊,名为“Hashed and Hierarchical Timing Wheels: Efficient Data Structures for Implementing a Timer Facility”的论文上。此文章指出,实现操作系统定时器模块的常规算法需要 O(n)的时间复杂度启动和维护计时器,对于更大问题规模 (n),这样的时间开销是巨大的,文中提出并证明了,通过一种环状桶的数据结构,可以做到使用 O(1)的时间复杂度,就可以启动,停止和维护计时器,并介绍了对时间间隔划分的处理,第一种方式是将所有的计时器时间间隔进行散列(Hash),这些时间间隔被散列到时间轮上特定的槽位中(Slot),第二种方式是利用多粒度定时轮组成具有层级结构的组合,以扩展更大的时间范围。这两种结构将在第二章中详细介绍。

1.2 基于队列的定时任务执行模型

在计算机的世界中,只有待解决的问题大规模化以后,算法的价值才能够得到最大化的体现。在介绍时间轮算法之前,我们有必要介绍另一种定时任务的实现,即基于队列的定时任务。队列这种数据结构无论是在操作系统中还是各编程语言如 Java 中都被大量使用,本文不再展开赘述。

下面从线程模型、定时任务种类和任务队列的数据结构三个方面展开详细介绍:

(1)线程模型

用户线程:负责定时任务的注册;轮询线程:负责从任务队列中扫描出符合执行条件的任务,例如任务的待执行时间已经到达,轮询线程将从队列中取出该任务,并交由异步线程池处理该任务。异步线程池:专门负责任务的执行。

(2)定时任务

定时任务主要分为一次性执行的定时任务(Dubbo 中超时判断)以及重复执行的定时任务,这两种定时任务都很好理解,一次性执行的定时任务在规定的未来某一时刻或距离现在的一段固定时长后执行,分别对应绝对值和相对值的概念。

而重复执行的定时任务是在一次性执行任务的基础上多次重复执行,这意味着,在上述线程协调工作中,当重复执行任务执行完成一次后,将被重新放回任务队列中。

(3)任务队列数据结构

从最简单的数据结构出发,假设我们选用最基本的队列,或者考虑到增减任务的方便,选择双向链表做为任务队列,为任务队列中的每个任务提供一个时间戳字段,这种实现的策略会产生哪些问题?

最大的问题是在查询上,假设任务队列中存在一些任务,那么为了找出达到规定时刻的待执行任务,轮询线程需要扫描全部任务,此种数据结构的时间复杂度为 O(n),而且存在大量的空轮询,即大部分的任务都没有达到执行时间,这种效率几乎是不可接受的。

为了提升查询效率,可以尝试从数据结构出发,利用有序队列,在计算机的算法中,有序性可以显著提高遍历的效率,这样一来,定时任务队列轮询线程从头向尾遍历时,在发现任意任务未达到规定执行时间戳后,就可以停止遍历。

但是维护有序性也需要付出代价,普通任务队列入队一个任务的时间复杂度仅仅是 O(1),而有序任务队列入队一个任务的时间复杂度为 O(nlogn)。其次,我们可以借鉴分治的思想,将任务队列分成 n 份,利用多线程遍历,在线程完全并发执行的情况下,问题规模简化到原来的 1/n。但是多线程也会 CPU 执行效率降低。

综上分析,定时任务框架需要具有如下要素:

  1. 严格高效的数据结构,并不能基于简单的队列结构来存储任务,否则轮询的执行效率永远无法提高。
  2. 简单的并发模型:CPU 的线程非常宝贵,不应占用过多线程资源。

时间轮算法解决了上述基于队列的定时任务执行模型的缺陷,因此时间轮算法思想在后面互联网技术发展中得到了大量应用,我们熟悉的 Linux Crontab,以及 Java 开发中常用的 Dubbo、Netty、Quartz、Akka、ZooKeeper、Kafka 等,几乎所有的时间任务调度都采用了时间轮算法的思想。

值得一提的是,在 Dubbo 中,为了增强系统容错,很多地方需要用到只需一次执行的任务调度,比如消费者需要知道各个 RPC 调用是否超时,而在 Dubbo 最开始的实现中,是采用将所有的返回结果(defaultFuture),都放入一个集合中,并通过一个定时任务,间隔扫描所有的 future,逐个判断是否超时。这样逻辑简单,但是浪费性能,后面 Dubbo 借鉴了 Netty,引入了时间轮。

第二章 时间轮算法思想介绍及应用场景介绍

2.1 时间轮简介

时间轮实质上是一种高效利用线程资源的任务调度模型,将大批量的任务全部整合进一个调度器中,从而对任务进行统一的调度管理,针对定时任务,延时任务等事件的调度效率非常高。

时间轮算法的核心是:第一章中描述的对任务队列进行轮询的线程不再负责遍历所有的任务,而是仅仅遍历时间刻度。时间轮算法好比指针不断在时钟上旋转、遍历,如果发现某一时刻上有任务(任务队列),那么就会将任务队列上的所有任务都执行一遍,这样便大幅度的减少了额外的扫描操作。

第一章中,我们提出了一个高效的定时任务框架需要具备严格高效的数据结构和简单的并发模型两个特点,而时间轮模型正是具备了这样的特点。

基于时间轮算法思想,后续也出现了很多种时间轮模型,目前流行的大致有三种,分别为简单时间轮模型、带有 round 的时间轮模型以及分层时间轮模型,下面将依次介绍这三种时间轮模型。

2.2 时间轮模型

2.2.1 简单时间轮模型

简单时间轮模型不再使用队列作为数据结构,而是使用数组加链表的形式(很经典的组合), 如下图所示,该时间轮通过数组实现,可以很方便地通过下标定位到定时任务链路,因此,添加、删除、执行定时任务的时间复杂度为 O(1)。

image

图 2.2.1 简单时间轮模型

显然,这种简单时间轮就解决了任务队列中遍历效率低下的问题,轮询线程遍历到某一个时间刻度后,总是执行对应刻度上任务队列中的所有任务(通常是将任务扔给异步线程池来处理),而不再需要遍历检查所有任务的时间戳是否达到要求。

通过增加槽(slot)的数量,可以细化的时间粒度以及得到更大的时间跨度,但是这样的实现方式有巨大的缺陷:

  1. 当时间粒度小,时间跨度大,而任务又很少的时候,时间槽的轮询效率变低。
  2. 当时间粒度小,时间槽数量多,而任务又很少时,很多槽位占用的内存空间是没有意义的。

2.2.2 带有 round 的时间轮模型

类比循环数组的思想,后人设计了带 round 的时间轮,这种时间轮的结构如下图所示:

image

图 2.2.2 带有 round 的时间轮模型

如图 2.2.2 所示,expire 代表到期时间,round 表示时间轮要在转动几圈之后才执行任务,也就是说当指针转到某个 bucket 时,不能像简单的单时间轮那样直接执行 bucket 下所有的任务。而且要去遍历该 bucket 下的链表,判断时间轮转动的次数是否等于节点中的 round 值,只有当 expire 和 round 都相同的情况下,才能执行任务。

这种结构的时间轮明显减少了所需刻度的个数,即弥补了简单时间轮在时间槽位较多,而任务较少情况下内存空间浪费的问题。

但是这种结构的时间轮并不能减少轮询线程的轮询次数,效率相对较低。

2.2.3 分层时间轮模型

分层时间轮也是一种对简单时间轮的改良方案,它的设计理念可以类比于日常生活中的时钟,分别有时、分、秒三个层级,并且每个轮盘分别具有 24、60、60 个刻度,因此,只需要 144 个刻度,即可表示一天的时间,而这种表示方式的优势在于,倍数级别时间表示的新增,只需要常数级别的刻度增加。例如,在 144 个刻度可表示的一天时间的基础上,新增 30 个刻度,即可精细表示一个月的时间。

image

图 2.2.3 分层时间轮模型

分层时间轮的工作方式为低层级的时间轮带动高层级的时间轮转动,图中箭头为任务的“下放”,例如,2 号 8 点 40 分 0 秒执行的任务,当天轮转动到刻度 2 时,会将第 2 天的任务,下放到对应时轮刻度为 8 的槽位中,当时轮转动到 8 时,会将任务继续下放到分轮刻度为 40 的槽位中,直至到最低层次的时间轮,转动到该槽位时,将该槽位中的任务,全部执行。

针对时间复杂度,这种时间轮对比带有 round 的时间轮不再遍历计算对比任务的 round,而是直接全部取出执行。

针对空间复杂度,分层时间轮利用维度上升的思路对时间轮进行分层,每个层级的时间粒度对应一个时间轮,多个时间轮之间进行级联协作。

2.3 时间轮应用场景介绍

时间轮作为高效的调度模型,在各种场景均有广泛的应用,常见的场景主要有如下几个:

(1)定时器

时间轮常用于实现定时器,可以在指定时间执行特定任务。定时器可以用于周期性任务、超时任务等,如轮询 I/O 事件、定期刷新缓存、定时清理垃圾数据等。

(2)负载均衡

时间轮可以用于实现负载均衡算法,将请求分配到不同的服务器上,避免单个服务器负载过重。时间轮可以根据服务器的负载情况来动态调整分配策略,实现动态负载均衡。

(3)事件驱动

时间轮可以用于实现事件驱动模型,将事件分配到不同的处理器上,提高并发处理能力。事件可以是 I/O 事件、定时事件、用户事件等,时间轮可以根据事件的类型和优先级来动态调整分配策略,实现高效的事件驱动模型。

(4)数据库管理

时间轮可以用于实现数据库管理,将数据分配到不同的存储设备上,提高数据读写效率。时间轮可以根据数据的类型、大小和访问频率等来动态调整数据分配策略,实现高效的数据库管理。

(5)其他应用

时间轮还可以用于其他一些应用,如消息队列、任务调度、网络流量控制等,具体应用取决于具体的需求和场景。

第三章 时间轮在 Dubbo 的应用与实现

3.1 Dubbo 中时间轮的应用

Dubbo 的设计中,客户端在调用服务端的时候,会对任务进行计时,如果任务超时,那么会被检测到,并重试请求。在 Dubbo 最开始的实现中,是采用将所有的返回结果(defaultFuture),都放入一个集合中,并通过一个定时任务,间隔扫描所有的 future,逐个判断是否超时。

这样逻辑简单,但是浪费性能,后面 Dubbo 借鉴了 Netty,引入了时间轮。任务交由时间轮管理,由专门的线程进行调度。

3.2 Dubbo 中时间轮的实现

Dubbo 中时间轮算法的实现,主要有一个类和三个接口:

image

首先是 Timer 接口,这个一个调度的核心接口,主要用于后台的一次性调度,我们仅介绍 newTimeOut 方法,这个方法就是把一个任务扔给调度器执行,第一个参数类型 TimerTask,即需要执行的任务。

image

接下来是 TimeTask 接口,它只有一个方法 run,参数类型是 Timeout,我们注意到上面 Timer 接口的 newTimeout   这个方法返回的参数就是 Timeout,和此处的入参相同,实际这里传入的 Timeout 参数就是 newTimeout 的返回值。

image

Timeout 对象与 TimerTask 对象一一对应,两者的关系类似于线程池返的 Future 对象与提交到线程池中的任务对象之间的关系。

最后是 TimeOut 接口,它代表的是对一次任务的处理,其中有几个方法,从介绍上即可看出各方法用途,这里不再赘述。

image

上述几个接口从逻辑上构成了一个任务调度系统。下面是任务调度系统的核心,即时间轮调度器的实现-- HashedWheelTimer。

仔细看它的类上注释可以发现,该方法并不能提供精确的计时,而是检测每个 tick 中(也就是时间轮中的一个时间槽),是否有 TimerTask,其期望执行时间已经落后于当前时间,如果是则执行该任务。任务执行时间的精确度可以通过细化时间槽来提升。

默认的 tick duration 是 100 毫秒,大部分网络应用中,I/O 超时并非必须是精准的,例如 5 秒超时,实际上稍晚一会也是可以的,因此这个默认值无需修改。

image

这个类维护了一种称为“wheel”的数据结构,也就是我们说的时间轮。简单地说,一个 wheel 就是一个 hash table,它的 hash 函数是任务的截止时间,也就是我们要通过 hash 函数把这个任务放到它应该在的时间槽中,这样随着时间的推移,当我们进入某个时间槽中时,这个槽中的任务也刚好到了它该执行的时间。

这样就避免了在每一个槽中都需要检测所有任务是否需要执行。在 HashedWheelTimer 的构造函数中,最重要的是 createWheel 方法,忽略基本的参数校验,只看方法主流程,首先是对时间槽数量的规范化处理,处理方式为将构造时传入的数量,修改为大于等于它的最小的 2 的次幂。为什么这样处理以及处理的具体方式,有兴趣可以研究下源代码。

image

接着则是创建时间槽数组,最后是初始化时间槽数组的每个参数。

下面介绍下 newTimeout 方法,这个方法的主要作用是向调度器中添加一个待执行的任务,同样忽略基本的参数校验,主体流程为:

  1. 第一步将等待调度的任务数+1,如果超过了最大限制,则-1 并抛出异常。
  2. 第二步则调用 start 方法,启动时间轮。
  3. 第三步计算当前任务的截止时间,并做防溢出处理。
  4. 构造一个 TimeOut ,并放入等待队列。

image

这里我们展开比较重要的 start 方法,首先获取 worker 的运行状态,如果是初始化状态,则更新成已启动状态,启动 workThread 线程,若是其他状态,则做其他相应的处理。接着是等待 workThread 将 startTime 初始化完成(在 Worker 的 run 方法中初始化完成),之所以需要等待 startTime 初始化完成,是因为 newTimeout 方法中,start 方法调用后也用到了这个 startTime,不这样做,任务的截止时间计算会有问题。

image

至此,我们介绍了利用 HashedWheelTimer 添加一个任务的主体流程,接下来是时间轮的内部运转。

首先是 HashedWheelTimer 的内部类 Worker,其中 run 方法的主体流程如下:

1.初始化 startTime,这里与上文中 start 方法内部对应。初始化后,利用闭锁 CountDownLatch 通知等待线程往下执行。

image

2.当定时器处于已启动状态时,不停地推进 ticket,推进的过程分解为:

  • 等待下一个 ticket 的到来。
  • ticket 到来后,计算该 ticket 对应时间轮的槽位(取模运算)。
  • 处理已取消的任务队列。
  • 获取当前时间槽,并将待处理任务队列中的任务放到槽中。
  • 执行当前时间槽中的任务。

image

3.如果时间轮已经停止了,则执行以下流程:

  • 清理所有时间槽中的未处理任务调度。
  • 清理待处理任务调度队列,将未取消的加入到未处理集合中。
  • 处理已取消的任务队列。

image

我们重点关注下定时器启动状态下的第 3 步,获取当前时间槽,并将待处理任务队列中的任务放到槽中的方法 transferTimeoutsToBuckets,其流程为以下几个步骤(这里规定循环了有限次,防止待处理队列过大,导致本次添加到槽耗费时间过长):

  • 从待处理任务调度队列中取出第一个任务,进行校验。
  • 根据取出的待处理任务调度,计算出一个槽。
  • 设置此任务调度的剩余圈数(从这里看出 Dubbo 用的是我们在 2.2.2 中介绍的“带有 round 的时间轮”)。
  • 取计算出的槽和当前槽中的较大者,并进行取模。
  • 将此任务调度加入对应的槽中。

image

总结:这部分内容我们分别从向调度器中添加任务的主体流程和时间轮内部运转两个部分,简单介绍了 Dubbo 中时间轮的实现。

如果感兴趣,可以学习其源代码,里面很多代码设计非常巧妙,比如 startTime 初始化及初始化完成后的线程间通信实现,这些设计思路对笔者这样的初学者来说很有益处。

第四章 时间轮算法的应用展望

笔者在刚开始工作时,设计过一个叫做下载中心的服务,这个服务的功能为导出和下载项目中的数据文件,实际的定位是为了减少异步线程过多而影响各个核心业务,因此将其功能抽取出来,从而达到减少核心业务压力的目标。

下载中心的初步设计,考虑到并发请求以及文件过大带来的内存溢出问题,除了采取各种方式避免外,整体思路是,预计特别大的文件,先将任务记录进行持久化,并通过后台线程池慢慢执行这些任务,通过任务记录,主动拉取数据、生成文件等。

模型类似于 Netty 中的 BOSS-WORKER 的模式,BOSS 线程负责定时从数据库中查出未消费的任务,并将其分配给 worker 线程池进行消费,如图 4.1 所示。

image

图 4.1 改造前应用服务任务消费模式示意图

当前设计虽然可以做到防止内存溢出等问题,但是这样的设计也存在一定的缺陷:

  1. 如果后续用户量增多,可以考虑水平扩充服务的数量,但是用于持久化任务记录的数据库会成为瓶颈。
  2. 即使 BOSS 线程不难做到避免任务的重复消费,但是待执行任务的查询效率会大大降低。
  3. 整个服务太过于依赖 BOSS 线程。

因此,考虑一种方式替代这种 BOSS-WORKER 模式,目前想到的一种方式为 MQ 消息队列,将待执行的任务信息投至 MQ 队列当中,然后该服务对其进行消费。这样的做法,即可解决上述 3 个问题,并具备维持任务有序性的优势。

但是内存易溢出的问题仍然存在,因此,考虑限制消费任务的线程并发数量。如果超过这个数量,则不再消费任务,而是重新投递任务至 MQ 队列中。这里,我们有更好的做法,即需要将任务重新投递至 MQ 队列时,做一些延时的处理,防止反复重新投递任务。整体流程如图 4.2 所示:

image

图 4.2 改造后应用服务任务消费模式示意图

图中,绿色模块为整个系统设计中的用以调度定时任务的任务调度模块,利用时间轮来统一管理这些定时任务。

对于短暂延时和长延迟的消息,我们都期望延时尽可能的精确,而对于长延时的消息,我们还要对其进行持久化,也就是暂存。等到消息快要到期时,再重新取出,进行投递。

而这种长延时消息的持久化,与我们图 4.1 所示定时从数据库取任务所遇到的瓶颈是一致的。

我们更期望有成熟的框架,能够提供 1.长延迟任务的持久化 以及 2. 任务调度 的能力。从中间件平台组提供的 MQ 中,我们发现目前它是已经支持 包含这两个能力的延迟 MQ 功能的。延迟 MQ 架构大致如下图所示:

image

图 4.3 延迟 MQ 消息处理流程图

具体延迟消息的发送和处理的流程如下图所示:

image

图 4.4 延迟消息发送和处理流程示意图

实际上,该延迟 MQ 的实现,正是由时间轮实现的调度 以及利用 MongoDB 数据库 实现的持久化,这与我们所期望的能力完全一致,完全可以满足我们的需求。

总结

本文从定时任务和时间轮算法的起源开始,对时间轮算法进行了介绍。详细的阐述了时间轮的算法思想,以及简单时间轮、带 round 的时间轮以及分层时间轮这三种常见的时间轮模型,并给出了对应的数据结构实现。

接着以 Dubbo 为例,介绍了时间轮模型在 Dubbo 中的应用,从源码出发,介绍了该算法在 Dubbo 中的主要实现。

最后,我们介绍了笔者自身所做过的一个小模块,展开分析了该模块功能目前所遇到的瓶颈,并给出了通过融合了时间轮算法的延迟 MQ 来优化当前设计的思路。

参考文献:
Hashed and Hierarchical Timing Wheels: EfficientData Structures for Implementing a Timer Facility.

所谓架构,意即
系统架构
,广义上它涵盖业务架构、运维架构、组织架构等所有系统构建场景,本文特指一般开发人员主要关注的
开发架构

关于架构的理论有很多,每个人也都有各自的理解,笔者相信很多人在实际运用中也会遇到各种各样的问题和困惑,本文抛开教条,从一个实际项目的演化看何为架构。

项目背景

开始之前,先了解下项目背景。该项目原本是为某东南亚公司开发的图库,提供图片使用授权服务,正规项目。无奈当地营商环境鱼龙混杂,黑白手段层出不穷,有好几个其它项目要么被 DDOS,要么流量被劫持,要么莫名出现违规内容,搞得该公司苦不堪言,这回干脆重金悬赏,遍求挑梁贤能。这不,被笔者一个在国内灵活就业的朋友揭了榜,几年之后才回国,这中间发生的种种又是另外的故事了。

不管怎么说,虽然项目看着不大,但除业务本身内容外,服务器和数据安全也须重点考虑。

基础版

显然,用户浏览网站需要一个
site
,如果采用前后端分离还得有
api-server

为了防止恶意用户上传违规内容,所有图片都由管理员上传至图床比如
AWS-S3(公共读私有写模式)

S3 同步版

基础版虽然在业务端保证了数据安全,但由于两处服务器(S3、site/api,即红色节点)直接对外暴露,黑客能轻而易举地实现定位进而展开攻击。S3 是可靠的云服务,倒也不用太担心,然而惊弓之鸟的甲方要求管理员不能直接访问 S3,防止黑客嗅探到管理端。

于是,在管理端和 S3 中间新加一个中转节点
minio
,图片先上传到 minio,再同步给 S3,如此,管理端暴露的风险大大降低。

注意此时还没有解决 site/api 暴露的问题,后面会讲。

会员版

马上新的需求出来了,甲方要求加入收费会员模式,但是不能出现任何关于公司的账户信息(看来甲方真的是怕了,要彻底地隐藏自己)。

要有会员体系,得先有账号模块,为防止恶意注册,加上邮箱验证即可。

收费不能体现公司账号比较麻烦,常规走银行流水肯定不行,第三方甚至套壳支付总是会被追踪到,剩下的选择只有加密货币了,所幸加密货币在当地是被广泛使用的。

加密货币交易一般都是异步的,所以要设计一个定时器,定时从链上获取最新的交易记录,同步给账号中心处理。

图中虚线表示连接的是内部服务。

CMS 版

管理员除管理图片外,还需对网站本身进行管理,比如广告投放、布局调整、会员策略等。于是新增了
CMS-Service

当然以后还会需要其它非 CMS 的站点管理功能比如会员管理、系统监控等,统一以
admin-site
对管理端提供服务。

CQRS 版

现在,把图片管理一同并入站点管理,并对各服务应用
CQRS
模式,清晰了业务边界,为后续业务扩展打下良好基础。

注意,其中 admin-site 兼具 site-command、album-command 职责。

IM 版

为了提升服务质量,甲方老板要求加上在线客服,客户诉求第一时间反馈,同时其它一些消息(比如客户下单、图片合约到期等),管理员也能第一时间收到。

这就不单单是一个即时聊天系统了,还得有消息推送的功能。

再考虑到当地朝九晚五懒散的工作状态,想要管理员一直呆在电脑前也不现实,那么客户消息和系统消息就容易错过。

还好,这里有一个人人都离不开的聊天 App ——
Telegram
,这玩意儿类似国内的微信,但是开放了很多接口和协议,使第三方系统能方便地接入它。比如我们可以使用它的
Telegram-Bot
,当客户发送消息时,IM 服务除向管理端实时推送外,还会推送给 Telegram-Bot,Telegram-Bot 再将消息推至 Telegram,如此,沉浸在美女频道的管理员就能及时知道有待反馈消息了。

Cloudflare 版

系统的主要功能基本都实现了,但甲方心心念念的安全问题反倒随着功能的扩展显得更加严峻。如之前所述,直接暴露给用户的节点风险等级最高,其次为管理员使用节点,进而会影响到整个系统内部。

自己搭建一套安全体系成本太高,实力也不允许,幸好市面上有免费的午餐,比如
Cloudflare

Cloudflare 是全球知名的网络服务商,提供保护、优化、加速网站等相关服务。本项目中至少可以考虑如下服务:

  • 源站 IP 隐藏
  • Bots - 自动屏蔽恶意爬虫
  • Turnstile - 人机验证
  • S3 域名重写为本站域名(做了 cname 解析)

同时要求管理员在所使用的设备上安装
VPN
,将风险降到最低。

可以看到,图中的红黄线块都没了,整个系统只要内部不出问题,从外部攻破的可能性不大。

CI/CD 版

还有一个漏洞,甲方意味深长地说。他指的是从本地开发/测试机部署至生产环境这道工序,同样会出现本地设备直连线上节点的情况。虽然现在服务器已经藏得够深,直接登录被发现的几率不大,但安全起见,甲方还是要求每次都采用跳板机中转一下。

对于经常发版的任务,跳板中转属实也有点麻烦,可以采用
CI/CD
方案,自动化部署,既轻松又安全。


总结

架构,既是名词,亦是动词,既是实体,亦是变化,它是一种理念,也是一种实施。但它肯定不是一套模板 —— 如果所有的房屋都千篇一律,那也不存在架构师了。

整个项目,甲方对安全的要求简直到了偏执的程度,很多要求看似无理,聪明的你也许能看出些端倪:)

Funcion Calling介绍

函数调用允许您将模型如gpt-4o与外部工具和系统连接起来。这对于许多事情都很有用,比如为AI助手赋能,或者在你的应用程序与模型之间建立深度集成。

如果您了解或者使用过Semantic Kernel可能会发现除了OpenAI支持Function Calling的模型之外,自动函数调用好像并不好用,国产大模型几乎都不能使用,由于想解决这个问题,在GitHub上找到了一个大佬的方法。

GitHub地址:
https://github.com/Jenscaasen/UniversalLLMFunctionCaller

大佬是通过提示工程与Semantic Kernel中调用本地函数的原理来做的,我看了大佬的代码,将提示词改为了中文,可能会更适用于国产大模型。

之前写了一篇文章:
如何让其他模型也能在SemanticKernel中调用本地函数
介绍了这个方法。

但是当时自己并没有开源项目,感兴趣的朋友,没有办法快速地上手体验,只能自己重新来一遍,现在已将这部分内容集成到我的开源项目SimpleRAG中,感兴趣的朋友只需填入自己的API Key即可快速体验,也可以方便地查看代码了。

GitHub地址:
https://github.com/Ming-jiayou/SimpleRAG

一种通用的Function Calling方法

在开始介绍之前,先看一下效果:

对比一下不使用FunctionCalling的效果:

image-20240828162455519

再来一个示例:

对比不使用Function Calling的效果:

image-20240828162754671

具体代码可在GitHub中查看,这里重点介绍一下实现的过程。

这里以Qwen2-7B-Instruct为例。

首先创建一个Kernel:

image-20240828163952619

在Kernel中导入插件:

image-20240828164048682

以上只是用于测试的模拟函数。

只需这样写即可:

image-20240828165221419

现在探究一下里面的过程。

首先将插件转化为文本:

image-20240828165354209

image-20240828165413278

在对话历史中加入示例:

image-20240828165513048

image-20240828165557673

在对话历史中加入一个指令:

image-20240828165704135

image-20240828165801213

将所有可用的函数嵌入到这个Prompt中了,如下所示:

image-20240828165901365

将指令加入到对话历史中了,如下所示:

image-20240828170031287

让LLM根据任务选择应该先调用哪个函数或者不用调用函数:

image-20240828170139513

LLM返回完成这个任务需要调用的函数:

image-20240828170317084

验证这个函数:

image-20240828170348135

调用插件中的函数:

image-20240828170514946

image-20240828170607398

image-20240828170626711

第一个函数返回的结果:

image-20240828170658135

再向LLM发送请求,现在该调用哪个函数,LLM的返回如下所示:

image-20240828170756097

同样执行插件中的第二个函数:

image-20240828170846964

第二个函数的返回:

image-20240828170917273

然后再向LLM发送请求:

image-20240828171024714

调用的函数名为Finished,表示流程已完成,可以跳出来了,如下所示:

image-20240828171128972

获得了最后的信息:

image-20240828171224711

结果如下所示:

image-20240828171253353

以上就是这个方法的大概流程,具体实现可以看GitHub开源的代码。

经过测试这种方法可用的LLM

平台 可用模型
硅基流动 Llama-3.1-405/70/8B、Llama-3-70/8B-Instruct、DeepSeek-V2-Chat、deepseek-llm-67b-chat、Qwen2-72/57/7/1.5B-Instruct、Qwen2-57B-A14B-Instruct、Qwen1.5-110/32/14B-Chat、Qwen2-Math-72B-Instruct、Yi-1.5-34/9/6B-Chat-16K、internlm2_5-20/7b-chat
讯飞星火 Spark Lite、Spark Pro-128K、Spark Max、Spark4.0 Ultra
零一万物 yi-large、yi-medium、yi-spark、yi-large-rag、yi-large-fc、yi-large-turbo
月之暗面 moonshot-v1-8k、moonshot-v1-32k、moonshot-v1-128k
智谱AI glm-4-0520、glm-4、glm-4-air、glm-4-airx、glm-4-flash、glm-4v、glm-3-turbo
DeepSeek deepseek-chat、deepseek-coder
阶跃星辰 step-1-8k、step-1-32k、step-1-128k、step-2-16k-nightly、step-1-flash
Minimax abab6.5s-chat、abab5.5-chat
阿里云百炼 qwen-max、qwen2-math-72b-instruct、qwen-max-0428、qwen2-72b-instruct、qwen2-57b-a14b-instruct、qwen2-7b-instruct

以上不一定完备,还有一些模型没测,欢迎大家继续补充。