2024年2月

操作系统 :CentOS 7.6_x64
FreeSWITCH版本 :1.10.9

日常开发中,会遇到需要在已存在的session上执行定时挂机和取消挂机的情况,今天整理下这方面的内容,我将从以下几个方面进行描述:

  • 实验场景环境描述
  • 定时挂机描述
  • 如何使用定时挂机
  • 如何取消定时挂机
  • 提供示例代码及运行效果视频

一、实验场景环境描述

FreeSWITCH测试机:192.168.137.32
会议室: test1
分机: 1000

模拟的场景:
1)会议室test1邀请分机1000加入会议室
在邀请时,添加定时挂机任务。
2)分机1000接通后,需要按数字1加入会议;
3)如果分机1000在特定时间内按数字1键,则正常入会
同时,取消定时挂机任务。
4)如果分机1000未在特定时间内按数字1键,则执行超时挂机操作。

二、定时挂机描述

FreeSWITCH提供了个定时挂机的工具,工具名称: sched_hangup

sched_hangup是dptools模块提供的工具,该工具会创建一个任务,用于执行定时挂机操作,该任务的信息存储在FreeSWITCH数据库的tasks表中进行持久化,这里进行简单描述。

1)函数调用链

调用链如下:

sched_hangup_function 
=> switch_ivr_schedule_hangup
=> switch_scheduler_add_task
=> switch_scheduler_add_task_ex

sched_hangup_function函数如下:

switch_ivr_schedule_hangup函数如下:

switch_scheduler_add_task函数和switch_scheduler_add_task_ex函数如下:

数据库写入操作:

2)任务执行

函数调用链如下:

main
=> switch_core_init_and_modload
=> switch_core_init
=> switch_scheduler_task_thread_start
=> switch_scheduler_task_thread
=> task_thread_loop
=> switch_scheduler_execute
switch_scheduler_task_thread_start 函数:

switch_scheduler_task_thread函数:

task_thread_loop函数:

switch_scheduler_execute函数:

也可以使用其它工具来实现定时挂机,原理是一样的,如何使用可参考场景实现部分。

三、如何使用定时挂机

FreeSWITCH自身提供的有定时挂机应用,可在拨号方案里面作为app使用,也可以在控制台作为api使用。

拨号方案中使用格式:

<actionapplication="sched_hangup"data="[+]<time>[ <hangup_cause>]"/>
控制台中使用格式:
sched_hangup [+]<time> <uuid>[ <hangup_cause>]

更多信息请参考官网wiki:

https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod-dptools/6587061/

比如创建如下任务:

sched_hangup +600 2efce360-2ce2-4470-8e92-01adb048892a
可在数据库和控制台看到,其中在控制台使用如下命令查询:
show tasks
具体效果如下:

四、如何取消定时挂机

可在控制台使用如下命令进行任务删除,进而取消定时挂机:

sched_del <taskid>

五、场景实现

这里以lua脚本为例来实现定时挂机和取消挂机。

1、外呼分机时添加lua脚本回调

可通过 execute_on_answer 来实现,具体如下:

bgapi originate {execute_on_answer='lua inviteToConf.lua'}user/1000 &bridge(loopback/wait)
originate​的使用可参考我之前写的文章:

2、获取任务id

sched_hangup 在创建任务时,会将任务id存放在数据库里面,如果要取消挂机任务,需要获取该id值。可通过如下方式获取:
1)根据session的uuid查询数据库获取任务id值;
2)使用"show tasks"解析获取任务id值;

在lua脚本里面如果想直接通过 executeString 的返回值获取,是无法办到的,因为 sched_hangup 给控制台的返回值只有 "+OK" 这个信息。

获取任务id值的方式,可从如下渠道获取:

关注微信公众号(聊聊博文,文末可扫码)后回复 20240213 获取。

3、添加拨号方案

添加转接会议室的拨号方案:

<extensionname="conf_test">
    <conditionfield="destination_number"expression="^7001$">
        <actionapplication="conference"data="test1"/>
        <actionapplication="hangup"/>
    </condition>
</extension>

4、运行效果

控制台执行originate后:
1)不按数字1,超时后会执行挂机操作;
2)按数字1,会删除挂机计划,邀请分机入会;

其中,按数字1后的效果如下:

运行效果视频可从如下渠道获取:

关注微信公众号(聊聊博文,文末可扫码)后回复 2024021301 获取。

六、资源下载

本文涉及资源可从如下渠道获取:

关注微信公众号(聊聊博文,文末可扫码)后回复 20240213 获取。

microsoftdotnet

在一篇博文中
我们对 .NET 9 的愿景
[1]: .NET团队概述了.NET 9的开发目标和最终愿景,涵盖两大重点领域:云原生和智能应用程序开发。它在继.NET 8之后,继续强化对云原生应用和性能的支持。.NET 9预览版1已经在2024年2月13日发布,与往常一样,.NET 9 将在整个平台上提供性能、生产力、安全性和其他改进。

.NET 9 的重点领域包括:

  • 云原生开发者平台:.NET 9对运行时性能和应用监控等基础功能进行了增强,同时提供了对流行生产基础设施和服务的支持,例如在Kubernetes中运行和管理数据库及缓存服务。在 .NET 8 中构建 Aspire 云就绪工作的基础上,.NET 团队将尝试通过对整个堆栈的改进来满足所谓的云原生应用程序的独特要求。与此相关的是,Microsoft今天还发布了
    .NET Aspire Preview 3
    [2]
  • 工具优化:为了提高开发效率,Visual Studio和Visual Studio Code将提供新的开发和部署体验,支持.NET Aspire和Azure部署,将继续与Canonical和Red Hat等行业合作伙伴密切合作,以确保.NET在任何地方都能正常运行。
  • 人工智能集成:.NET 9 将更轻松地将 AI 功能集成到现有和新应用中。这项工作将包括 OpenAI 和 OSS AI 模型(托管和本地)的库集成到现有和新建应用的过程,提供了丰富的库和文档支持。

此外,.NET 9还包含了许多其他新特性和项目更新,例如对Native AOT的应用程序优化、改进的垃圾回收机制,以及对ASP.NET Core应用的安全加固。
.NET团队还在GitHub上为.NET 9设定了反馈和更新,鼓励开发者和用户参与讨论,提供反馈,共同推动.NET 9的发展。

总的来说,.NET 9是一个面向云原生和智能应用开发的重大进步,它通过提高性能、生产力和安全性,以及平台的全方面升级,为开发者提供了更加丰富和高效的选择。有兴趣入门的开发人员
可以立即下载 .NET 9 预览版 1
[3]。与往常一样,Microsoft 计划以可预测的节奏定期发布更新,并将在 11 月的 .NET Conf 上发布 .NET 9。

相关链接:

原文 | Richard Lander

翻译 | 郑子铭

WebAssembly(Wasm)
是一种令人兴奋的新虚拟机和(汇编)指令格式。 Wasm 诞生于浏览器,是
Blazor
项目的重要组成部分。 Wasm 的第二个行动是针对应用程序和功能的云计算。 WebAssembly 系统接口 (WASI) 是新的推动者,为 WebAssembly 代码提供了一种安全地跨语言调用和实现任意 API 的方法。现在可以使用 .NET 8 中的 wasi 实验工作负载通过 .NET 创建 WASI 应用程序。我们正在探索这些新技术并在此环境中运行 .NET 应用程序……。真的,任何地方。

这篇文章将帮助您了解 Wasm 的广泛使用,并描述 .NET 已经可以实现的功能。他们说历史不会重演,但会押韵。我们又回来进行另一轮“一次编写,随处运行”。 WASI 应用程序是可移植的二进制文件,可以在任何硬件或操作系统上运行,并且不特定于任何编程语言。这一次,感觉不一样了。这不仅仅是供应商的神经;一切都是中立的。

Wasm 和 WASI

Wasm 可能会为我们提供云计算的重启,并承诺提供单一云原生二进制文件、更高的密度和更便宜的多租户。出于同样的原因,它也开启了边缘计算的可能性。事实上,
CloudFlare

Fastly
已经使用 Wasm 在边缘托管公共计算。

Wasm 与在 Linux 容器中运行应用程序不同,后者是对现有标准和代码的(良好且聪明的)重新打包。 Wasm 更像是在没有操作系统的环境中运行应用程序,只有汇编代码、内存和对外部世界的标准化(和门控)访问(通过 WASI)。

Build 2023 上的 Hyperlight
演示(4m 视频)深入了解了支持 Wasm 的云的外观。它演示了在新的轻量级安全虚拟机管理程序中运行的 Blazor 应用程序。 Hyperlight 激发了新托管范例的想象力。

WebAssembly 系统接口 (WASI)

WebAssembly 接口类型 (WIT)

WebAssembly 组件模型
是最新一轮 Wasm 创新的关键规范。它们基本上仍处于设计阶段并
正在经历重大变化
。这篇文章(以及 .NET 8 实现)以 WASI Preview 1 为中心。我们希望 .NET 9 实现使用 WASI Preview 2。

WIT 和
wit-bindgen
使用任何源语言编写的组件都可以与主机系统进行通信。
WIT 对 C# 支持的实现

@silesmo
领导。 Wasm 和 WIT 一起定义了
应用程序二进制接口(ABI)

我们期望 WASI 成为一组标准的 WIT 类型,提供对
低级功能
的访问(例如
获取时间

读取文件
)。这些低级类型有效地形成了跨编程语言和操作系统的“Wasm 标准库”。例如,我们从来没有 Rust 开发人员和 .NET 开发人员可以同时使用的标准和共享功能。历史上还没有任何广泛部署的本机代码公开具有
OO
形状(如接口)的 API,可以跨编程语言和操作系统使用。

标准 WIT 类型以 wasi- 开头,定义“平台”。您可以将它们视为与 .NET 中的系统命名空间类似的方式(与 WASI 中的“S”匹配)。继续类比,您可以在 System 命名空间之外创建自己的 .NET 命名空间,WIT 也是如此。

这些帖子在更详细地构建 WASI 方面做得非常出色。

即将到来的承诺是能够采用现有的 .NET 应用程序或库并将其编译为 Wasm 目标。我们的设计本能是在 .NET 堆栈中实现相对较高的 WIT 接口(例如为
wasi-sql
创建 ADO.NET 数据提供程序),这将使现有代码(包括许多现有的 NuGet 包)能够正常工作,特别是对于没有本机依赖项。

Wasm 应用程序在 Wasm 运行时中运行,例如
wasmtime
。与 Docker 非常相似,您可以使用特定功能配置该运行时。例如,如果您希望 Wasm 代码能够访问键/值存储,您可以
向其公开一个键/值接口
,该接口可以由本地数据库或云服务支持。

Wasm 运行时旨在可嵌入到应用程序中。事实上,有一个 wasmtime 包用于
在 .NET 应用程序中托管 Wasm
。 .NET 代码可以作为 Wasm 运行,但 .NET 应用程序可以托管 wasmtime?!?是的,这个空间开始看起来是圆形的。虽然这些场景看起来很循环,但它们
最终可能非常有用
,与
AppDomain
的使用方式大致相似。这也让人想起所有“docker in docker”场景。
我们期待更多的创新、更多的 Wasm 运行时和更多的行业参与者。事实上,Wasm 已经升级为
W3C 规范
。 W3C 是 Wasm 的完美家园,让它成长为广泛的行业规范,就像之前的 HTML 和 XML 一样。

wasi-实验工作量

.NET 8 包含一个名为 wasi-experimental 的新工作负载。它构建在 Blazor 使用的 Wasm 功能之上,将其扩展为在 wasmtime 中运行并调用 WASI 接口。它还远未完成,但已经实现了有用的功能。

让我们从理论转向演示新功能。

安装
.NET 8 SDK
后,您可以安装 wasi-experimental 工作负载。

dotnet workload install wasi-experimental

注意:此命令可能需要管理员权限,例如在 Linux 和 macOS 上使用 sudo。

您还需要安装
wasmtime
来运行您即将生成的 Wasm 代码。

使用 wasi-console 模板尝试一个简单的示例。

$ dotnet new wasiconsole -o wasiconsole
$ cd wasiconsole
$ cat Program.cs 
using System;

Console.WriteLine("Hello, WASI Console!");
$ dotnet run
WasmAppHost --runtime-config /Users/rich/wasiconsole/bin/Debug/net8.0/wasi-wasm/AppBundle/wasiconsole.runtimeconfig.json
Running: wasmtime run --dir . -- dotnet.wasm wasiconsole
Using working directory: /Users/rich/wasiconsole/bin/Debug/net8.0/wasi-wasm/AppBundle
Hello, WASI Console!

该应用程序使用 wasmtime 运行。这里没有 x64 或 Arm64,只有 Wasm。

dotnet run 提供额外的信息(在控制台输出中)来帮助解释发生了什么。未来这种情况可能会改变。与主机系统的所有交互均由 wasmtime 管理。

我们可以更深入地查看 AppBundle 目录。

$ ls -l bin/Release/net8.0/wasi-wasm/AppBundle
total 24872
-rwxr--r--  1 rich  staff  66666691074 Oct 31 07:53 dotnet.wasm
-rwxr--r--  1 rich  staff   1526128 Oct 11 14:00 icudt.dat
drwxr-xr-x  6 rich  staff       192 Nov 19 19:35 managed
-rwxr-xr-x  1 rich  staff        48 Nov 19 19:35 run-wasmtime.sh
-rw-r--r--  1 rich  staff       915 Nov 19 19:35 runtimeconfig.bin
drwxr-xr-x  2 rich  staff        64 Nov 19 19:35 tmp
-rw-r--r--  1 rich  staff      1457 Nov 19 19:35 wasiconsole.runtimeconfig.json
$ ls -l bin/Release/net8.0/wasi-wasm/AppBundle/managed 
total 3432
-rw-r--r--  1 rich  staff    27136 Nov 19 19:35 System.Console.dll
-rw-r--r--  1 rich  staff  1711616 Nov 19 19:35 System.Private.CoreLib.dll
-rw-r--r--  1 rich  staff     5632 Nov 19 19:35 System.Runtime.dll
-rw-r--r--  1 rich  staff     5120 Nov 19 19:35 wasiconsole.dll

SDK 将应用程序发布到独立部署中。 .NET 运行时 — dotnet.wasm — 已经编译为 Wasm(在我们的构建机器上)。应用程序和 dotnet.wasm 在 wasmtime 中一起加载,运行所有代码。应用程序的实际托管代码(位于托管目录中)在运行时解释,就像 Blazor WebAssembly 一样。
@yowl

@SingleAccretion
社区成员一直在尝试
Wasm 和原生 AOT

您可能想知道为什么我们需要将所有这些文件分开,而显然更好的选择是拥有一个 wasiconsole.wasm 文件。我们也可以这样做,但稍后会在帖子中介绍它,因为我们需要在机器上安装更多的软件(目前 wasi 实验工作负载不包含这些软件)。

RuntimeInformation 告诉我们什么?

RuntimeInformation 是我最喜欢的类型之一。它让我们更好地了解目标环境。

我们可以稍微更改示例以显示一些更有用的信息。

using System;
using System.Runtime.InteropServices;

Console.WriteLine($"Hello {RuntimeInformation.OSDescription}:{RuntimeInformation.OSArchitecture}");
Console.WriteLine($"With love from {RuntimeInformation.FrameworkDescription}");

它产生这个输出。

Hello WASI:Wasm
With love from .NET 8.0.0

第一行很有趣。操作系统是WASI,架构是Wasm。这是有道理的,有更多的背景。文章前面提到 Wasm 可以被认为是“无操作系统”,但是我们不能简单地称之为 Wasm,因为现有的浏览器和 WASI 环境有很大不同。因此,该环境唯一一致的名称是 WASI,而 Wasm 明确是“芯片架构”。

Wasm 是一个 32 位计算环境,这意味着 2^32 字节是可寻址的。但是,Wasm 运行时可以配置为使用
memory64
,从而可以访问 >4GB 的内存。我们还没有对此的支持。

访问主机文件系统

Wasmtime(和其他 Wasm 运行时)提供将主机目录映射到来宾目录的选项。从用户的角度来看,这与使用 Docker 进行卷挂载类似,但实现细节有所不同。

让我们看一个依赖目录安装的简单应用程序。它使用 Markdig 包
将 markdown 转换为 HTML
。公平地说,
Markdig
并不是为了以 Wasm 的身份运行而编写的。只要能够为其创建一个舒适的管理环境,Markdig 就会很高兴,这就是我们所做的。

让我们在 Mac M1 (Arm64) 机器上尝试一下。

$ pwd
/Users/rich/git/wasm-samples/tomarkup
$ dotnet publish
$ cd bin/Release/net8.0/wasi-wasm/AppBundle 
$ cat run-wasmtime.sh
wasmtime run --dir . dotnet.wasm tomarkup $*
$ ./run-wasmtime.sh 
A valid inputfile must be provided.
$  wasmtime run --dir . --mapdir /markdown::/Users/rich/markdown --mapdir /tmp::/Users/rich dotnet.wasm tomarkup $* /markdown/README.md /tmp/README.html
$ ls ~/*.html
/Users/rich/README.html
$ cat ~/markdown/README.md | head -n 3  
# .NET Runtime

[![Build Status](https://dev.azure.com/dnceng-public/public/_apis/build/status/dotnet/runtime/runtime?branchName=main)](https://dev.azure.com/dnceng-public/public/_build/latest?definitionId=129&branchName=main)
$ cat ~/README.html | head -n 3       
<h1>.NET Runtime</h1>
<p><a href="https://dev.azure.com/dnceng-public/public/_build/latest?definitionId=129&amp;branchName=main"><img src="https://dev.azure.com/dnceng-public/public/_apis/build/status/dotnet/runtime/runtime?branchName=main" alt="Build Status" /></a>
<a href="https://github.com/dotnet/runtime/labels/help%20wanted"><img src="https://img.shields.io/github/issues/dotnet/runtime/help%20wanted?style=flat-square&amp;color=%232EA043&amp;label=help%20wanted" alt="Help Wanted" /></a>

--mapdir 正在挂载从主机到来宾的目录。

如您所见,
Markdown 文件
已转换为 HTML。为了简洁起见,显示了每个文件的前三行。

目录挂载所需的 CLI 手势目前有点不方便。这是我们需要在未来版本中考虑的内容。这实际上是一个 dotnet run 和 wasmtime run 应该如何关联的问题。

但它能算字数吗?

我最近出版了
《System.IO 的便利》
,重点关注字数统计。我们能否获得与 Wasm 相同的代码来运行并看看它的运行速度有多快?

该文章中的字数统计基准测试在 Linux x64 上运行。让我们保持不变,但这次以 Wasm 身份运行。

$ pwd
/Users/rich/git/convenience/wordcount/count
$ grep asm count.csproj 
    <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
    <WasmSingleFileBundle>true</WasmSingleFileBundle>
$ dotnet publish
$ cd bin/Release/net8.0/wasi-wasm/AppBundle/
$ WASMTIME_NEW_CLI=0 wasmtime run --mapdir /text::/home/rich/git/convenience/wordcount count.wasm $* /text/Clarissa_Harlowe
    11716  110023  610515 /text/Clarissa_Harlowe/clarissa_volume1.txt
    12124  110407  610557 /text/Clarissa_Harlowe/clarissa_volume2.txt
    11961  109622  606948 /text/Clarissa_Harlowe/clarissa_volume3.txt
    12168  666666908  625888 /text/Clarissa_Harlowe/clarissa_volume4.txt
    12626  108593  614062 /text/Clarissa_Harlowe/clarissa_volume5.txt
    12434  107576  607619 /text/Clarissa_Harlowe/clarissa_volume6.txt
    12818  112713  628322 /text/Clarissa_Harlowe/clarissa_volume7.txt
    12331  109785  611792 /text/Clarissa_Harlowe/clarissa_volume8.txt
    11771  104934  598265 /text/Clarissa_Harlowe/clarissa_volume9.txt
        9     153    1044 /text/Clarissa_Harlowe/summary.md
   109958  985714  5515012 total

我更新了
项目文件
以包含
wasi-wasm

true
并注释掉 PublishAot 相关属性。我还添加了一个
runtimeconfig.template.json
文件。未对应用程序代码进行任何更改。

现在,我们将整个应用程序放在一个文件包中。

$ ls -l bin/Release/net8.0/wasi-wasm/AppBundle/
total 6684
-rw-r--r-- 1 rich rich    1397 Nov 19 19:59 count.runtimeconfig.json
-rwxr-xr-x 1 rich rich 6827282 Nov 19 19:59 count.wasm
-rw-r--r-- 1 rich rich     915 Nov 19 19:59 runtimeconfig.bin
-rwxr-xr-x 1 rich rich      27 Nov 19 19:59 run-wasmtime.sh
drwxr-xr-x 2 rich rich    4096 Nov 19 19:59 tmp

看起来好多了。该应用程序只有不到 7MB。我必须安装
WASI-SDK
才能使用 WasmSingleFileBundle 属性并设置环境变量以使 dotnetpublish 能够找到所需的工具。

$ echo $WASI_SDK_PATH
/home/rich/wasi-sdk/wasi-sdk-20.0/

wasmtime 最近发生了重大变化。我选择使用
WASMTIME_NEW_CLI=0
来恢复运行示例的旧行为。

让我们回到性能。首先,作为 wasm 运行(通过解释器执行托管代码):

$ time WASMTIME_NEW_CLI=0 wasmtime run --mapdir /text::/home/rich/git/convenience/wordcount count.wasm $* /text/Clarissa_Harlowe
    11716  110023  610515 /text/Clarissa_Harlowe/clarissa_volume1.txt
    12124  110407  610557 /text/Clarissa_Harlowe/clarissa_volume2.txt
    11961  109622  606948 /text/Clarissa_Harlowe/clarissa_volume3.txt
    12168  666666908  625888 /text/Clarissa_Harlowe/clarissa_volume4.txt
    12626  108593  614062 /text/Clarissa_Harlowe/clarissa_volume5.txt
    12434  107576  607619 /text/Clarissa_Harlowe/clarissa_volume6.txt
    12818  112713  628322 /text/Clarissa_Harlowe/clarissa_volume7.txt
    12331  109785  611792 /text/Clarissa_Harlowe/clarissa_volume8.txt
    11771  104934  598265 /text/Clarissa_Harlowe/clarissa_volume9.txt
        9     153    1044 /text/Clarissa_Harlowe/summary.md
   109958  985714  5515012 total
Elapsed time (ms): 821
Elapsed time (us): 821223.8

real    0m0.897s
user    0m0.846s
sys 0m0.030s

现在有了我们对 Wasm 的(甚至更多)
实验性原生 AOT 支持

$ time WASMTIME_NEW_CLI=0 wasmtime run --mapdir /text::/home/rich/git/convenience/wordcount count.wasm $* /text/Clarissa_Harlowe
    11716  110023  610515 /text/Clarissa_Harlowe/clarissa_volume1.txt
    12124  110407  610557 /text/Clarissa_Harlowe/clarissa_volume2.txt
    11961  109622  606948 /text/Clarissa_Harlowe/clarissa_volume3.txt
    12168  666666908  625888 /text/Clarissa_Harlowe/clarissa_volume4.txt
    12626  108593  614062 /text/Clarissa_Harlowe/clarissa_volume5.txt
    12434  107576  607619 /text/Clarissa_Harlowe/clarissa_volume6.txt
    12818  112713  628322 /text/Clarissa_Harlowe/clarissa_volume7.txt
    12331  109785  611792 /text/Clarissa_Harlowe/clarissa_volume8.txt
    11771  104934  598265 /text/Clarissa_Harlowe/clarissa_volume9.txt
        9     153    1044 /text/Clarissa_Harlowe/summary.md
   109958  985714  5515012 total
Elapsed time (ms): 60
Elapsed time (us): 60322.2

real    0m0.107s
user    0m0.064s
sys 0m0.045s

现在,在 Linux x64 上使用 CoreCLR 运行:

$ time ./app/count ../Clarissa_Harlowe/
    11716  110023  610515 ../Clarissa_Harlowe/clarissa_volume1.txt
    12124  110407  610557 ../Clarissa_Harlowe/clarissa_volume2.txt
    11961  109622  606948 ../Clarissa_Harlowe/clarissa_volume3.txt
    12168  666666908  625888 ../Clarissa_Harlowe/clarissa_volume4.txt
    12626  108593  614062 ../Clarissa_Harlowe/clarissa_volume5.txt
    12434  107576  607619 ../Clarissa_Harlowe/clarissa_volume6.txt
    12818  112713  628322 ../Clarissa_Harlowe/clarissa_volume7.txt
    12331  109785  611792 ../Clarissa_Harlowe/clarissa_volume8.txt
    11771  104934  598265 ../Clarissa_Harlowe/clarissa_volume9.txt
        9     153    1044 ../Clarissa_Harlowe/summary.md
   109958  985714  5515012 total
Elapsed time (ms): 77
Elapsed time (us): 77252.9

real    0m0.128s
user    0m0.096s
sys 0m0.014s

这些都是有趣的结果。我们有解释、AOT 和 JIT 代码生成方法可供比较。 Wasm 解释器能够在不到一秒的时间内计算(略低于)一百万个单词,而 AOT 编译的 Wasm 和 JIT 运行时可以在大约 100 毫秒内完成同样的操作。

注意:Main 方法是运行 main 的时间,由 StopWatch 测量。流程是整个流程的持续时间,以时间来衡量。

此图表显示了上下文中的所有结果,包括
System.IO 的便利性
帖子中的结果。

wasmtime JIT 将 Wasm 代码编译到目标环境(在本例中为 Linux+x64)。例如,可以使用
wamr
对 Wasm 代码进行 AOT。我将把它留到另一篇文章中。

原文链接

Extending WebAssembly to the Cloud with .NET

知识共享许可协议

本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接:
http://www.cnblogs.com/MingsonZheng/
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (
MingsonZheng@outlook.com
)

Semantic Kernel 内置的
IChatCompletionService
实现只支持 OpenAI 与 Azure OpenAI,而我却打算结合 DashScope(阿里云模型服务灵积) 学习 Semantic Kernel。

于是决定自己动手实现一个支持 DashScope 的 Semantic Kernel Connector —— DashScopeChatCompletionService,实现的过程也是学习 Semantic Kernel 源码的过程,
而且借助
Sdcb.DashScope
,实现变得更容易了,详见前一篇博文
借助 .NET 开源库 Sdcb.DashScope 调用阿里云灵积通义千问 API

这里只实现用于调用 chat completion 服务的 connector,所以只需实现
IChatCompletionService
接口,该接口继承了
IAIService
接口,一共需要实现2个方法+1个属性。

public sealed class DashScopeChatCompletionService : IChatCompletionService
{
    public IReadOnlyDictionary<string, object?> Attributes { get; }

    public Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }

    public IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}

先实现
GetChatMessageContentsAsync
方法,调用
Kernel.InvokePromptAsync
方法时会用到这个方法。

实现起来比较简单,就是转手买卖:

  • 把 Semantic Kernel 的
    ChatHistory
    转换为 Sdcb.DashScope 的
    IReadOnlyList<ChatMessage>
  • 把 Semantic Kernel 的
    PromptExecutionSettings
    转换为 Sdcb.DashScope 的
    ChatParameters
  • 把 Sdcb.DashScope 的
    ResponseWrapper<ChatOutput, ChatTokenUsage>
    转换为 Semantic Kernel 的
    IReadOnlyList<ChatMessageContent>

实现代码如下:

public async Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default)
{
    var chatMessages = chatHistory
        .Where(x => !string.IsNullOrEmpty(x.Content))
        .Select(x => new ChatMessage(x.Role.ToString(), x.Content!)).
        ToList();

    ChatParameters? chatParameters = null;
    if (executionSettings?.ExtensionData?.Count > 0)
    {
        var json = JsonSerializer.Serialize(executionSettings.ExtensionData);
        chatParameters = JsonSerializer.Deserialize<ChatParameters>(
            json,
            new JsonSerializerOptions { NumberHandling = JsonNumberHandling.AllowReadingFromString });
    }

    var response = await _dashScopeClient.TextGeneration.Chat(_modelId, chatMessages, chatParameters, cancellationToken);

    return [new ChatMessageContent(new AuthorRole(chatMessages.First().Role), response.Output.Text)];
}

接下来实现
GetStreamingChatMessageContentsAsync
,调用
Kernel.InvokePromptStreamingAsync
时会用到它,同样也是转手买卖。

ChatHistory

PromptExecutionSettings
参数的转换与
GetChatMessageContentsAsync
一样,所以引入2个扩展方法
ChatHistory.ToChatMessages

PromptExecutionSettings.ToChatParameters
减少重复代码,另外需要将
ChatParameters.IncrementalOutput
设置为
true

不同之处是返回值类型,需要将 Sdcb.DashScope 的
IAsyncEnumerable<ResponseWrapper<ChatOutput, ChatTokenUsage>>
转换为
IAsyncEnumerable<StreamingChatMessageContent>

实现代码如下:

public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(
    ChatHistory chatHistory,
    PromptExecutionSettings? executionSettings = null,
    Kernel? kernel = null,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    var chatMessages = chatHistory.ToChatMessages();
    var chatParameters = executionSettings?.ToChatParameters() ?? new ChatParameters();
    chatParameters.IncrementalOutput = true;

    var responses = _dashScopeClient.TextGeneration.ChatStreamed(_modelId, chatMessages, chatParameters, cancellationToken);

    await foreach (var response in responses)
    {
        yield return new StreamingChatMessageContent(new AuthorRole(chatMessages[0].Role), response.Output.Text);
    }
}

到这里2个方法就实现好了,还剩下很容易实现的1个属性,轻松搞定

public sealed class DashScopeChatCompletionService : IChatCompletionService
{
    private readonly DashScopeClient _dashScopeClient;
    private readonly string _modelId;
    private readonly Dictionary<string, object?> _attribues = [];

    public DashScopeChatCompletionService(
        IOptions<DashScopeClientOptions> options,
        HttpClient httpClient)
    {
        _dashScopeClient = new(options.Value.ApiKey, httpClient);
        _modelId = options.Value.ModelId;
        _attribues.Add(AIServiceExtensions.ModelIdKey, _modelId);
    }

    public IReadOnlyDictionary<string, object?> Attributes => _attribues;
}

到此,DashScopeChatCompletionService 的实现就完成了。

接下来,实现一个扩展方法,将 DashScopeChatCompletionService 注册到依赖注入容器

public static class DashScopeServiceCollectionExtensions
{
    public static IKernelBuilder AddDashScopeChatCompletion(
        this IKernelBuilder builder,
        string? serviceId = null,
        Action<HttpClient>? configureClient = null,
        string configSectionPath = "dashscope")
    {
        Func<IServiceProvider, object?, DashScopeChatCompletionService> factory = (serviceProvider, _) =>
            serviceProvider.GetRequiredService<DashScopeChatCompletionService>();

        if (configureClient == null)
        {
            builder.Services.AddHttpClient<DashScopeChatCompletionService>();
        }
        else
        {
            builder.Services.AddHttpClient<DashScopeChatCompletionService>(configureClient);
        }

        builder.Services.AddOptions<DashScopeClientOptions>().BindConfiguration(configSectionPath);
        builder.Services.AddKeyedSingleton<IChatCompletionService>(serviceId, factory);
        return builder;
    }
}

为了方便通过配置文件配置 ModelId 与 ApiKey,引入了
DashScopeClientOptions

public class DashScopeClientOptions : IOptions<DashScopeClientOptions>
{
    public string ModelId { get; set; } = string.Empty;

    public string ApiKey { get; set; } = string.Empty;

    public DashScopeClientOptions Value => this;
}

最后就是写测试代码验证实现是否成功,为了减少代码块的长度,下面的代码片段只列出其中一个测试用例

public class DashScopeChatCompletionTests
{
    [Fact]
    public async Task ChatCompletion_InvokePromptAsync_WorksCorrectly()
    {
        // Arrange
        var builder = Kernel.CreateBuilder();
        builder.Services.AddSingleton(GetConfiguration());
        builder.AddDashScopeChatCompletion();
        var kernel = builder.Build();

        var prompt = @"<message role=""user"">博客园是什么网站</message>";
        PromptExecutionSettings settings = new()
        {
            ExtensionData = new Dictionary<string, object>()
            {
                { "temperature", "0.8" }
            }
        };
        KernelArguments kernelArguments = new(settings);

        // Act
        var result = await kernel.InvokePromptAsync(prompt, kernelArguments);

        // Assert
        Assert.Contains("博客园", result.ToString());
        Trace.WriteLine(result.ToString());
    }

    private static IConfiguration GetConfiguration()
    {
        return new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json")
            .AddUserSecrets<DashScopeChatCompletionTests>()
            .Build();
    }
}

最后的最后就是运行测试,在 appsettings.json 中添加模型Id

{
  "dashscope": {
    "modelId": "qwen-max"
  }
}

注:
qwen-max
是通义千问千亿级大模型

通过 user-secrets 添加 api key

dotnet user-secrets set "dashscope:apiKey" "sk-xxx"

dotnet test
命令运行测试

A total of 1 test files matched the specified pattern.
博客园是一个专注于提供信息技术(IT)领域知识分享和技术交流的中文博客平台,创建于2004年。博客园主要由软件开发人员、系统管理员以及对IT技术有深厚兴趣的人群使用,用户可以在该网站上撰写和发布自己的博客文章,内容涵盖编程、软件开发、云计算、人工智能等多个领域。同时,博客园也提供了丰富的技术文档、教程资源和社区互动功能,旨在促进IT专业人士之间的交流与学习。

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms - SemanticKernel.DashScope.IntegrationTest.dll (net8.0)

测试通过!连接 DashScope 的 Semantic Kernel Connector 初步实现完成。

完整实现代码放在 github 上,详见
https://github.com/cnblogs/semantic-kernel-dashscope/tree/v0.1.0

1 背景

互联网场景下,我们经常会面临一个产品流量从初创时期的小流量到全盛大流量的过程。
这时候,原本的架构设计就显得很不合理,变成你追求服务稳定性阻碍。
然而这一切并不一定是你的架构能力的问题,而是在小流量场景下,不能过高的去评估容量和架构冗余性,避免造成不必要的资源和维护人力的浪费。
能做的是为以后的架构演进提供可扩展性准备,让原本强依赖数据存储层的风险可以实现逐层降级。

2 强依赖降级过程

2.1 第一阶:简单流量,读写不分离

如果你的业务刚上线,服务流量很小,那你大概率数据服务会仅部署一个实例,读写也会汇聚在一些。结构如下:
image
这样做的好处是:

  • 小流量场景下资源利用率高
  • 一致性高,不会出现读写顺序和一致性问题

这样做的坏处是:

  • 读写互相影响,一旦出现故障,整体不可用

2.2 第二阶:流量上涨,读大于写,读写分离

大厂内部的评估法则通常流量达到一定规模,并且读写比不低于8:2,否则读写分离的价值不大。结构如下:
image

读写分离这种数据库架构策略,将数据库的读和写操作分散到不同的数据库服务器上。这种策略有助于提高数据库的并发处理能力和整体性能。

读写分离的基本思想是将数据库的读操作和写操作分开处理,通常使用一个主数据库(Master)来处理写操作(如INSERT、UPDATE、DELETE等),而使用多个从数据库(Slave)来处理读操作(如SELECT等)。主数据库会将其更改实时同步到一个或多个从数据库中,确保数据的一致性。

它有如下优势:

  • 负载均衡:分发读操作到多个从数据库服务器,以提高并发处理能力和负载均衡。
  • 监控和故障转移:检测数据库服务器的健康状况,并在主数据库出现故障时自动切换到从数据库。

也存在一些挑战和限制:

  • 主从同步可能会引入延迟,导致从数据版本差异
  • 读写分离也可能增加数据一致性和故障恢复的复杂性

2.3 第三阶:多域流量和稳定性要求高,两地三中心

image

“两地三中心” 是指本地和异地分别建立三个中心,包括本地生产中心、同城灾备中心和异地灾备中心。这种架构旨在确保业务的高可用性和数据的安全性,以应对各种自然灾害或人为因素导致的业务中断。

从图中『核心指标』可以看出,它主要提高了可用性,即使在大规模流量场景下,能够保证系统足够健壮。
详细可以参考笔者这一篇《
高可用架构,去中心化有多重要?

2.4 第四阶:使用缓存进行风险降级

缓存的目的主要是兜底,避免持久化数据层出现故障时候的完全不可用。
同时能提高性能和可用性,毕竟高速缓存和磁盘的效率不可同日而语,多了一层保障,可用性也大幅提升。
详细可以参考笔者这边《
深刻理解高性能Redis的本质
》。

image

2.5 第五阶:去中心化,本地缓存提升可用性

互联网性能演进经常会听到这样一句话:把数据放在离用户最近的地方,即安全又快捷。
无论读取数据库还是读取缓存服务,毕竟是跨节点的,那就存在风险,网络抖动,机房故障都有可能成为故障诱因。
去中心化的本质是在所有依赖项都无法正常运转之后,服务依然独立可用。
详细可以参考笔者这一篇《
高可用架构,去中心化有多重要?

这边的做法就是在本地缓存一份刚性数据,允许一定的延迟,但是需要保证服务不被挂起,短时间内(一般4小时为判断标准)服务有依然可用。如下图:
image

3 总结

本文介绍了互联网场景下高流量的数据强依赖的降级演进过程。主要核心点如下:

  • 读写分离保证数据存储单点故障的恢复,如:Master故障,Slave切换成Master;Slave故障,读写汇聚到Master
  • 两地三中心提高可用性,应对地域级风险
  • 缓存服务提高性能和可用性,为数据中心故障做兜底
  • 本地缓存实现去中心化,进一步提高可用性,依赖项故障依然能保证服务独立可用