2024年10月

尽管
Streamlit
的使用非常直观,但正确的环境配置对于充分发挥其潜力仍然至关重要。

本篇将介绍如何从头开始配置
Streamlit
环境,以及
Streamlit
开发过程中常用的几个命令。

最后通过一个简单的示例演示开发
Streamlit
应用的过程。

1. 安装

Streamlit
是纯
Python
的框架,只依赖
Python
环境,

目前最新的
Streamlit v1.39
版本,需要
Python3.8
及以上的版本。

Streamlit
已经发布到
pypi
,使用
pip
安装非常简单。

pip install streamlit 

安装完成后,验证是否安装成功使用下面的命令:

streamlit hello

这个
Streamlit
中自带的示例工程,如果安装成功,执行之后会自动打开浏览器,


http://localhost:8501/
显示示例工程。

一共有
4个Demo
,可以从左半边的菜单中点开感受下
Streamlit
的魅力。

2. 常用子命令

Streamlit
子命令不多,通过
--help
参数可以查看。

$  streamlit --help
Usage: streamlit [OPTIONS] COMMAND [ARGS]...

  Try out a demo with:

      $ streamlit hello

  Or use the line below to run your own script:

      $ streamlit run your_script.py

Options:
  --log_level [error|warning|info|debug]
  --version                       Show the version and exit.
  --help                          Show this message and exit.

Commands:
  activate  Activate Streamlit by entering your email.
  cache     Manage the Streamlit cache.
  config    Manage Streamlit's config settings.
  docs      Show help in browser.
  hello     Runs the Hello World script.
  help      Print this help message.
  run       Run a Python script, piping stderr to Streamlit.
  version   Print Streamlit's version number.

最常用的是
run
子命令,这是用来执行
Streamlit App
的,
run
子命令本身也有很多的参数,

比如,App的IP地址,端口,主题,日志,自动重载脚本等等。

下面的命令可以查看
run
子命令的所有参数。

$  streamlit run --help

此外,
config
子命令可以快速查看当前对
Streamlit
的所有配置。

$  streamlit config show

cache
子命令可以用来快速清理缓存。

$  streamlit cache clear

开发过程中,用的较多的就是上面三个子命令。

3. 第一个App

最后,我们用
Streamlit
来做一个简单的数据分析的应用,以此体会下它其强大之处。

3.1. 创建测试数据

首先创建一些测试数据,通过
pandas

numpy
创建
20条
时间序列数据。

# 创建时间序列测试数据
A = np.random.randint(1, 80, size=(20, 1))
B = np.random.randint(20, 100, size=(20, 1))
df = pd.DataFrame()
df.index = pd.date_range("2024/10/01", periods=20)
df["A"] = A
df["B"] = B

A列

B列
是随机生成的数据,每次运行都会改变。

3.2. 用表格数据

接下来就是
Streamlit
登场的时候了,页面上显示
pandas

DataFrame
数据很简单,就一行代码。

# 显示数据
st.table(df)

浏览器访问:
http://localhost:8501/

可以加个标题,稍微美化一下。

st.header("第一个APP")
st.divider() # 一条分割线

3.3. 用折线图显示数据

Streamlit

表格
显示数据只要一行代码,同样,用
折线图
显示数据也只要一行代码。

# 显示折线图
st.line_chart(df)

3.4. 动态改变数据范围

接下来,添加
Streamlit
的控件,让我们可以动态的改变表格和折线图中的数据范围。

date_range = st.slider(
    "日期范围",
    min_value=datetime(2024, 10, 1),
    max_value=datetime(2024, 10, 20),
    value=(datetime(2024, 10, 1), datetime(2024, 10, 20)),
)
st.write(date_range)

添加一个数据范围的控件,范围改变时,用
date_range
的实际值去更新页面要显示的数据。

# graph_data是按日期过滤后的数据
graph_data = df.copy()
graph_data = graph_data[graph_data.index >= date_range[0]]
graph_data = graph_data[graph_data.index <= date_range[1]]

表格和折线图中的数据改成上面的
graph_data

# 显示折线图
st.line_chart(graph_data)

# 显示数据
st.table(graph_data)

这样,我们就可以在页面上动态改变数据范围,同时更新数据表格和折线图。

4. 总结

短短几行代码,就生成了一个展示
DataFrame
数据的
Web应用


传统的Web开发方式
相比,不需要任何前端的知识(
HTML

CSS

javascript
等),

而且,通过使用封装好的控件(
table

line_chart
等),开发效率极高。


Jupyter Notebook
相比,为用户提供了一个友好的操作界面,简单直观。

不需要用户通过修改代码来尝试不同的图表。

示例最终的完整代码如下:

import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime

# 创建时间序列测试数据
A = np.random.randint(1, 80, size=(20, 1))
B = np.random.randint(20, 100, size=(20, 1))
df = pd.DataFrame()
df.index = pd.date_range("2024/10/01", periods=20)
df["A"] = A
df["B"] = B

st.header("第一个APP")
st.divider()

# 增加日期范围动态调整
date_range = st.slider(
    "日期范围",
    min_value=datetime(2024, 10, 1),
    max_value=datetime(2024, 10, 20),
    value=(datetime(2024, 10, 1), datetime(2024, 10, 20)),
)
st.write(date_range)

graph_data = df.copy()
graph_data = graph_data[graph_data.index >= date_range[0]]
graph_data = graph_data[graph_data.index <= date_range[1]]

# 显示折线图
st.line_chart(graph_data)

# 显示数据
st.table(graph_data)


run
子命令来运行这个脚本即可。

streamlit run main.py

作者:来自 vivo 互联网服务器团队- Gao Meng

本文介绍了一种基于 sentinel 进行二次开发的动态限流解决方案,包括什么是动态限流、为什么需要引入动态限流、以及动态限流的实现原理。

一、背景

1.1 当前的限流方案

随着互联网的发展及业务的增长,系统的流量和请求量越来越大,针对高并发系统,如果不对请求量进行限制,在流量突增时可能会导致系统崩溃或者服务不可用,影响用户体验。因此,系统需要引入限流来控制请求的流量,保证系统的可用性和稳定性。当前推荐业务使用公司vsentinel 限流工具,主要使用
QPS 限流

热点参数限流

QPS 限流:对某个资源(通常为接口或方法,也可以自定义资源)的 QPS /并发数进行限流;热点参数限流:对某些具体的参数值进行限流,避免因为热点参数的过度访问导致服务宕机。

1.2 存在的问题

无论是 QPS 限流还是热点参数限流,都是对资源/参数的
定量限流
,即对某个资源/参数设置固定阈值,超过阈值则进行限流。

回到业务,游戏推荐系统作为游戏分发的平台,向公司内所有主要流量入口(包括游戏中心、应用商店、浏览器等)分发游戏、小游戏、内容和评论,具有大流量、高负载的业务特点。同时,游戏推荐系统对接的场景多(600+),单个性化接口有100+场景调用(场景可以理解为接口请求的一个基本请求参数)。当前的限流方案存在以下几个问题:

  1. 参数级别的限流,600+场景,无法做到每个场景
    精细化限流

  2. 接口级别的限流,不会区分具体的场景,无法保证
    核心场景
    的可用性;

  3. 如果场景流量有变更,需要及时调整限流阈值,不易
    维护

  4. 场景的流量会实时变化,无法做到根据流量变化的
    动态限流。

鉴于以上限流问题,推荐系统需要一个能够根据参数流量变化而
动态调整限流阈值

精细化限流
方案。

二、动态限流介绍

从配置方式上来看,动态限流和 QPS 限流、热点参数限流最大的不同之处在于,动态限流不是通过配置固定阈值进行限流,而是
配置每个参数的优先级,根据参数的优先级动态调整限流阈值。

图片

动态限流将资源和参数进行绑定,首先配置资源(一般是接口/方法)总的限流阈值,进而配置资源下具体参数的优先级,根据参数配置的优先级和实时流量,决定当前请求pass or block。

下图示例中,资源总的限流阈值为150,参数A、B、C、D的 QPS 均为100,且配置的参数优先级 A>B>C>D。

  • 参数A优先级最高,且 QPS(A) = 100 < 限流阈值150,所以A的流量全部通过;

  • 参数B优先级仅次于参数A,且 QPS(A) = 100 < 限流阈值150、QPS(A)  + QPS(B)= 200 > 限流阈值150,所以参数B部分流量通过,pass : 50,block:50;

  • 参数C和其它参数的优先级低于参数B,且 QPS(A)  + QPS(B)= 200 > 限流阈值150,所以参数C和其它参数均被限流。

图片

如果此时参数值A的 QPS 变为200,B、C的 QPS 仍为100,通过动态限流实现:A请求部分通过,B请求全部拦截,C请求全部拦截;根据各参数值流量的变化,动态适配各参数值通过/拦截的流量,从而实现根据参数值动态限流的效果。

总结:动态限流本质上是
参数优先级限流

支持对参数值配置优先级,根据参数值的优先级进行动态流控
。当流量超过阈值后,优先保证高优先级参数通过,拦截优先级低的参数请求。

三、sentinel 介绍

由于动态限流是基于 sentinel 进行二次开发,且动态限流的实现算法是基于 sentinel QPS 限流的优化,这里首先介绍下 sentinel 实现原理和 sentinel QPS 限流的滑动窗口计数器限流算法。

3.1 sentinel 原理介绍

sentinel 是阿里开源的一款面向分布式、多语言异构化服务架构的流量治理组件。主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。(官网描述)

sentinel 主要通过责任链模式实现不同模式的限流功能,责任链由一系列 ProcessorSlot 对象组成,每个 ProcessorSlot 对象负责不同的功能。

ProcessorSlot  对象可以分为两类:一类是辅助完成资源指标数据统计的 slot,一类是实现限流降级功能的 slot。

辅助资源指标数据统计的  ProcessorSlot:

  • NodeSelectorSlot:负责收集资源路径,并将调用路径树状存储,用于后续根据调用路径来限流降级;

  • ClusterBuilderSlot:负责存储资源的统计信息以及调用者信息,例如该资源的 RT、QPS、线程数等等,作为多维度限流、降级的依据;

  • StatisticSlot:负责实现指标数据统计,从多个维度(入口流量、调用者、资源)统计响应时间、并发线程数、处理失败数量、处理成功数量等指标信息。

实现限流降级功能的 slot:

  • ParamFlowSlot:用于根据请求参数进行限流(热点参数限流),例如根据某个参数的 QPS 进行限流,或者根据某个参数的值进行限流;

  • SystemSlot:用于根据系统负载情况进行限流,例如 CPU 使用率、内存使用率等。

  • AuthoritySlot:用于根据调用者身份进行限流,例如根据调用者的 IP 地址、Token 等信息进行限流。

  • FlowSlot:用于根据 QPS 进行限流,例如每秒最多只能处理多少请求。

  • DegradeSlot:用于实现熔断降级功能,例如当某个资源出现异常时,可以将其熔断并降级处理。

图片

除了上述原生 ProcessorSlot,sentinel 还支持 SPI 插件功能,通过实现 ProcessorSlot 接口自定义 slot,从而能实现个性化功能拓展。动态限流正是基于 sentinel SPI 插件方式实现。

3.2 滑动窗口计数器算法

sentinel 的 QPS 限流采用滑动窗口计数器算法,下面我们简单介绍下这个算法原理。

首先介绍一下计数器算法。

3.2.1 计数器

计数器算法:维护一个固定单位时间的计数器来统计请求数,在计数小于限流阈值时通过请求,计数到达限流阈值后拦截请求,直到下一个单位时间再重新计数。假设资源限制 1 秒内的访问次数不能超过 100 次。

  • 维护一个计数器,每次有新的请求过来,计数器加 1;

  • 收到新请求后,如果计数器的值小于限流值,并且与上一次请求的时间间隔还在 1秒内,允许请求通过,否则拒绝请求;如果超出了时间间隔,要将计数器清零。

图片

计数器算法存在一个问题:窗口切换时可能会出现流量突刺(最高2倍)。极端情况下,假设每秒限流100,在第1s和第2s分别通100个请求,且第1s的请求集中在后半段,第2s的请求集中在前半段,那么其实在500ms到1500ms这个1s的时间段,通过了200个请求。

图片

为了解决这个问题,引入了基于滑动窗口的计数器算法。

3.2.2 滑动窗口计数器

滑动窗口计数器算法是计数器算法的改进,解决了固定窗口的流量突刺问题。算法原理:

  • 将时间划分为细粒度的区间,每个区间维持一个计数器,每进入一个请求则将计数器加1;

  • 多个区间组成一个时间窗口,每到一个区间时间后,则抛弃最老的一个区间,纳入新区间;

  • 若当前窗口的区间计数器总和超过设定的限制数量,则本窗口内的后续请求都被丢弃。

图片

滑动窗口本质上是固定窗口更细粒度的限流,将单位时间划分多个窗口,划分的窗口越多,数据越精确。

四、基于 sentinel 的动态限流方案

动态限流是基于 sentinel 的二次开发,具体实现流程和 sentinel 的 QPS 限流类似,可以归纳为三步:数据统计、规则管理、流量校验。

  • 数据统计:统计资源(接口/方法/参数)的流量;

  • 规则管理:管理限流规则,维护资源的限流阈值及参数值优先级;

  • 流量校验:对比统计到的流量和对应的限流规则,决定当前请求 pass or block。

4.1 数据统计

动态限流的数据统计同 sentinel 流量控制模块一样,使用滑动窗口计数器算法统计当前的流量。

具体来讲,sentinel 流量控制中的数据统计,是将1s的时间窗细分为多个窗口,按窗口维度统计资源信息,包括请求总数、成功总数、异常总数、总耗时、最小耗时、最大耗时等。

动态限流的数据统计,同样是将1s的时间窗细分为多个窗口,不同的是窗口的统计维度是各个参数值通过的总流量。

具体实现上,每个资源有唯一的 bucket,bucket 内维护一个固定数量的滑动窗口,窗口中的 value 是一个 hash 结构,hash key 为限流参数的参数值,value 为参数值在当前时间窗口的请求量。

图片

参数值流量统计流程:

  1. 系统收到请求后,首先找到当前资源的 bucket;

  2. 再根据当前时间戳对 bucket 内的窗口数量取余,定位到当前时间窗;

  3. 当前时间窗内参数值的请求量+1。

4.2 规则管理

规则管理模块:配置和管理限流规则。

限流规则通过zk实现从后台到端上的同步。后台配置好限流规则后,将限流规则同步到zk;客户端监听zk消息变更,同步最新的限流规则。

图片

4.3 流量校验

4.3.1  参数临界点

对于动态限流而言,参数的限流阈值不是固定的,只有参数优先级的概念,所以校验的第一步是要找到限流
阈值优先级的临界点。

图片

如果参数优先级临界点已知,只需要判断流量参数的优先级大小。如果请求的优先级高于阈值参数的优先级,pass;反之,如果请求的优先级低于阈值参数的优先级,block;优先级相等,按接口阈值限流。

那么如何确认当前限流的优先级呢?

4.3.2 细分窗口

当前限流阈值配置一般为秒级别的限流,细分滑动窗口,就是将1s的窗口划分为N个更小的时间窗,只要N足够大,就可以将前N-1个窗口已经统计到的参数流量近似当做这一秒的流量,进而就可以计算出临界参数的优先级。具体来讲,每一个窗口中都记录了参数的请求数量,所以只要将前N-1个窗口的流量累加,就可以得到各个参数在当前这1s内的总请求量;之后按照参数的优先级从高到低,依次累加流量并与阈值比较,如果累加到某个参数时大于限流阈值,则这个参数对应的优先级即为限流阈值优先级的临界点。

图片

上面分析都是基于最理想情况:将1s的窗口无限细分。考虑到滑动窗口粒度越小,统计数据计算的越准确,但同时占用的资源也越多,计算越复杂,时延也越高,所以在实际应用中,1s的窗口不可能无限细分,是否有更好的优化方案呢?

4.3.3 动态预测

上面是将1s的窗口划分为N个更小的时间窗,将前N-1个窗口近似看成1s,利用前N-1个窗口的统计数据,来判断当前窗口是否需要限流。

N-1→ N → 1s,N越大误差越小,反之N越小误差就越大,为了弥补N大小引起的计算误差,将统计窗口朝前挪一个,即用最近1s已有的统计数据,来判断当前窗口是否需要限流。

换一种说法:用最近1s已有的统计数据计算临界点参数,预测当前窗口的请求是否需要限流。如果当前请求参数的优先级高于临界点参数,pass;低于临界点参数,block;等于临界点参数,部分通过。

图片

综上:动态限流采用
细分窗口
+
动态预测
的方法计算当前限流参数的优先级阈值。

举例说明:对方法 method(String param) 配置动态限流,限流阈值为120,配置 param 具体参数值的优先级为A→ 1, B→ 2, C→ 3(按重要程度划分 A > B > C);假设窗口大小为100ms,即1s细分为10个滑动窗口。每次开始新窗口流量计数时,先统计前10个窗口中各参数的请求量,继而按照优先级从高到低进行累加,确认优先级阈值;比如统计到前10个窗口中参数A, B, C的请求量均为100,因为A的流量100 < 阈值120,A + B的流量200 > 阈值120,所以此时临界参数为B;窗口接收到新请求后,比较请求参数和临界参数的优先级,比如参数A的请求,因为A的优先级高于B, pass;参数B的临界参数请求,允许部分流量通过;参数C的优先级低于临界参数,block。

4.3.4 double check

经过上面的分析可知,通过滑动窗口+动态预测的方案就可以找到临界点参数,进而根据参数优先级决定当前请求 pass or block。但是在实际时间窗和统计时间窗之间,有一个时间 gap,在这个时间窗内的流量计算有一定的滞后性,比如上面的例子,在新窗口中A的请求全部 pass,如果此时A的流量突刺到1000,那么总体通过的流量就会超过阈值,如下图所示。

图片

由上图可知,在流量突增的一个时间窗内,当前方案通过的流量会有突刺,为了解决流量突增带来的突刺问题,使用 double check 进行校验;check1 为细分窗口+动态预测方案,通过 check1 的流量可能会有突刺;增加 check2 对资源进行限流,保证被保护资源通过的总流量不超过阈值。

图片

double check 流量在应对流量突增时的流量情况:

图片

4.4 整体架构

复用 sentinel 责任链+ SPI 架构,使用独立 SDK 打包方式嵌入动态限流模板,不影响原 sentinel 处理流程,按需引入。

图片

4.5 实现效果

动态限流配置生效后,可通过监控查看各配置参数通过/拒绝的请求量,实现限流功能的可视化。

图片

如上图所示,配置某个资源的单机限流阈值为50,这一秒内的总请求量为74,通过50个请求,拒绝14个请求(其中配置限流的参数 xx.scene.priority1、xx.scene.priority2、xx.scene.priority3 在这一秒通过的请求数量分别为2个、16个、2个;其它参数通过30个请求,拒绝14个请求)。

解释:在这一秒内,配置的三个参数总请求量为20(2+16+2),小于阈值50,全部通过;其它参数总流量为44,这一秒的总请求量为64(20+44),大于限流阈值50,所以其它参数共通过30个请求,拒绝14个请求。

五、总结

本文介绍了一种基于 sentinel 进行二次开发的动态限流解决方案,提供更细粒度、能够根据流量动态调整限流阈值的参数级限流方法,是对 sentinel 限流功能的补充和拓展。

  • 对比 sentinel 的 QPS 限流,动态限流方案提供了更细粒度的参数级别的限流;

  • 对比 sentinel 的热点参数限流,热点参数限流是对参数的定量限流,适用于参数大流量场景,比如对某个频繁请求的参数(id/商品)进行限流;动态限流根据配置的参数优先级(重要程度)进行限流,限流的阈值参数会根据资源的流量动态调整,pass 高优参数,block 低优参数,限流重点是“保核心”。

综上,动态限流是对 sentinel 限流功能的补充,用户可以结合具体场景配置不同的限流方案。

在前端开发的世界中,随着项目的复杂性增加,如何高效管理样式,快速开发出响应式、美观的界面成为每个开发者关心的问题。
Tailwind CSS
作为一个革命性的
实用类
(utility-first)CSS 框架,以其灵活的样式管理方式赢得了广大开发者的青睐,目前是
GitHub

Star 数最多
的 CSS 类框架,充分说明了它在开发者中的流行程度。本文将详细介绍
Tailwind CSS
的显著特性、使用方式以及适用场景,深入分析为什么它成为现代前端项目中的首选工具。

简要介绍

Tailwind CSS
是一个不同于传统框架的 CSS 工具库。与
Bootstrap
等框架提供一系列预定义组件不同,
Tailwind
提供的是一组
高度可定制
的实用类,通过组合这些类,开发者能够自由设计出他们需要的界面,而不必依赖于预先设计好的 UI 元素。其实用类的核心设计理念让开发者可以直接在 HTML 中编写样式,极大提升了开发效率。

显著特性:

  1. 实用类优先

Tailwind CSS
最大的特点就是其
实用类设计
。每个 CSS 类都承担一个简单、明确的功能,比如 text-center 用于居中对齐文本,p-4 为元素添加 1rem 的内边距。开发者可以通过这些基础的类组合,快速构建复杂的 UI,而无需写自定义 CSS。

  1. 高度可定制

Tailwind
的配置文件(tailwind.config.js)允许开发者对框架进行
高度定制
。你可以调整颜色、字体、间距等,也可以根据项目需求扩展更多的类。这种灵活性让它能适应任何类型的项目,无论是简单的静态页面,还是复杂的 Web 应用程序。

  1. 内置响应式设计

Tailwind
预设了多种
响应式断点
(sm, md, lg, xl, 2xl),通过简单的类名可以快速创建响应式布局。无论是手机、平板还是桌面端设备,Tailwind 都能让界面适应不同屏幕尺寸。

  1. 小体积和性能优化

Tailwind
提供了高效的
按需生成
(purge)机制,在生产环境中,只保留实际使用到的 CSS 类,极大地减少了文件体积,提高了加载速度。这让它在性能上具有显著优势,特别适合需要优化资源的项目。

  1. 丰富的插件生态

Tailwind
提供了多种官方和社区维护的插件,开发者可以根据项目需求扩展 Tailwind 的功能。例如,tailwindcss/forms 插件可以优化表单元素的样式,tailwindcss/typography 提供优雅的排版样式。

使用方式

  1. 安装并创建tailwind.config.js配置
// 安装
npm install -D tailwindcss

// 创建配置文件
npx tailwindcss init
  1. 修改配置文件
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
  1. 将 Tailwind 指令添加到 CSS 中
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. 直接使用样式
<h1 class="text-3xl font-bold underline">
    Hello world!
</h1>

适用场景

Tailwind CSS
是一个适合各类项目的通用工具,以下场景特别适合选择 Tailwind:

  1. 快速开发

如果你需要快速构建一个具有响应式设计的网站或应用,
Tailwind CSS
是理想的选择。其内置的实用类和响应式设计让你无需编写复杂的 CSS,自然加快了开发速度。

  1. 灵活定制 UI

对于那些不希望局限于预定义组件的项目,
Tailwind
提供了极高的灵活性。你可以完全根据需求设计和定制 UI,无需与框架默认样式“斗争”。

  1. 希望最大程度优化性能的项目

Tailwind
的按需生成机制让它在性能优化上独树一帜。特别是对于大型项目,按需剔除未使用的 CSS 类能显著减少打包文件的大小,提升应用的加载性能。

  1. 与现代框架集成

无论你使用的是
Vue

React
还是
Next.js
等现代前端框架,
Tailwind
都能无缝集成,让样式管理变得更加高效和模块化。

为什么选择 Tailwind CSS?

Tailwind CSS
的最大魅力在于它的
极简设计

灵活性
,它让开发者能够在保持代码可读性和维护性的同时,高效地构建用户界面。与传统 CSS 框架相比,
Tailwind
不提供组件化的限制,开发者可以随心所欲地设计布局,创造出独一无二的界面风格。而在性能上,
Tailwind
的按需生成机制更是为那些追求性能的项目提供了极大的优化空间。

作为
GitHub Star
数最多的 CSS 类框架,
Tailwind CSS
已经证明了它在现代前端开发中的主导地位。如果你正在寻找一个能够提高开发效率、灵活适应项目需求,并且有良好性能表现的 CSS 框架,
Tailwind CSS
是你不容错过的选择。

总结

Tailwind CSS

实用类设计

高度定制性
和出色的
性能优化
,使它成为当前最受欢迎的 CSS 框架之一。无论是快速开发,还是构建高性能、响应式的网站,
Tailwind
都能为你提供强大的支持。如果你还没有使用过
Tailwind CSS
,不妨尝试一下,亲身体验它为开发工作带来的便利和高效。


该框架已经收录到我的全栈前端一站式开发平台 “前端视界” 中(浏览器搜 前端视界 第一个),感兴趣的欢迎浏览使用!

非线性激活函数对深度神经网络的成功至关重要,选择合适的激活函数可以显著影响其性能。大多数网络使用固定的激活函数(例如,
ReLU

GELU
等),这种选择可能限制了它们的表达能力。此外,不同的层可能从不同的激活函数中受益。因此,基于可训练激活函数的兴趣日益增加。

论文提出了一种基于有效微分同胚变换(称为
CPAB
)的可训练高表达能力激活函数
DiTAC
。尽管只引入了极少量的可训练参数,
DiTAC
仍然增强了模型的表达能力和性能,通常会带来显著的改善。它在语义分割、图像生成、回归问题和图像分类等任务中优于现有的激活函数(无论这些激活函数是固定的还是可训练的)。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Trainable Highly-expressive Activation Functions

Introduction


激活函数(
AFs
)在深度神经网络的成功中扮演着重要角色,因为它们赋予了后者非线性特性。实际上,激活函数对于网络能够近似几乎任意复杂的函数、学习有意义的特征表示以及实现高预测性能至关重要。除了其非线性特性,激活函数还具有各种特性,这些特性直接影响着网络的性能。传统的激活函数,如
Logistic Sigmoid

Tanh Unit
,会将输入值映射到一个较小的范围,这可能导致网络梯度接近零,从而影响训练性能。修正线性单元(
ReLU
)及其变种(例如,
LReLU

PReLU
)部分解决了这个问题,通过将输入映射到一个在一个或两个方向上无限制的范围内。指数激活函数如
ELU
继承了
ReLU
的优点,但也将激活函数的响应推向零均值,以提高性能。

一般来说,固定的激活函数(
AFs
)具有有限的非线性(因此表达能力有限),并对网络施加了学习偏差。因此,将它们调整为不同的问题类型和数据复杂性是具有挑战性的。因此,研究增强表达能力并缓解这种偏差的激活函数设计是一个开放的研究领域。可训练激活函数(
TAFs
)如
PReLU

Swish

PELU
通过添加几个可学习参数来调整标准固定激活函数的形状。根据研究,这类函数在表达能力上仅取得了微小的提升,因为这些
TAFs
的性能往往与其基本的不可训练激活函数相似。
Maxout
单元提供了另一种激活函数的方法。尽管在解决分类任务方面相较于
ReLU
有显著改进,但
Maxout
层中的参数数量会随着网络中神经元数量的增加而增加。

微分同胚是一个可微的可逆函数,具有一个可微的逆。论文提出了一种基于微分同胚的可训练激活函数(
Diffeomorphism-based Trainable Activation function
,
DiTAC
),这是一种基于高度表达和高效微分同胚(称为
CPAB
)的可微参数化
TAF
。尽管
DiTAC
仅添加了可以忽略不计的可训练参数,但它的表达能力仍然非常强。如图
1
所示,与现有的
TAFs
相比,这些
TAFs
仅限于学习某种特定形状或仅学习凸函数,而
DiTAC
能够学习多种形状。之前有研究展示了不同的激活函数适合于不同类型的数据和任务,这一事实激励了像这样的灵活
TAF
方法。特别地,
DiTAC
在各种数据集和任务(如语义分割、图像生成、图像分类和回归问题)上取得了显著的改进。

总之,论文的贡献如下:

  1. 第一个提出在可训练激活函数中使用灵活微分同胚的研究者。
  2. 呈现了
    DiTAC
    ,这是一种新颖的高度表达的激活函数,它解决了现有可训练激活函数的问题,并且可以轻松应用于任何模型架构。
  3. 展示了
    DiTAC
    在各种任务和数据集上优于现有的激活函数和可训练激活函数。
  • CPAB transformations in Deep Learning


Freifeld
等人提出的
CPAB
变换是一种高效且表达能力强的参数微分同胚。它们被称为
CPAB
,源于
CPA-Based
,因为它们基于连续分段仿射(
Continuous Piecewise-Affine
,
CPA
)速度场。自其诞生以来,这些变换在深度学习(
DL
)中找到了许多应用。所有这些工作使用
CPAB
变换与论文使用它们之间的主要区别在于,在那些工作中,
CPAB
变换总是应用于目标信号的域(无论是
2D
图像中的空间域还是时间序列中的时间域),通常是通过将它们融入空间变换网络(
Spatial Transformer Net
,
STN
)或时间变换网络(
Temporal Transformer Net
,
TTN
)中,而论文则将其(逐元素)应用于特征图的范围(顺便提一下,这也意味着不需要进行网格重采样,这是
STNs
/
TTNs
中的一个必需步骤)。图
2
说明了这一区别。特别地,论文是第一批使用
CPAB
变换(或者说任何其他高表达性微分同胚家族)来构建可训练激活函数(
TAFs
)的。

Method


Preliminaries: 1D CPAB Transformations

处理微分同胚通常涉及昂贵的计算,由于在深度学习架构中直接使用微分同胚,因此降低相关计算负担变得尤为重要(相较于非深度学习应用)。设
\(T^{\theta}\)
为由
\({\theta}\)
参数化的微分同胚,在训练过程中,数量
\(x \mapsto T^{\theta}(x)\)

\(x \mapsto \nabla_{\theta} T^{\theta}(x)\)
需要在多个
\(x\)
值和多个
\({\theta}\)
值下进行计算。

选择
CPAB
变换作为使用的微分同胚家族的主要原因是它们既具有表达能力又高效。在剩下的部分中,所有的
CPAB
变换都假定是在一维的。


\(\Omega=[a,b]\subset \mathbb{R}\)
为一个有限区间,且设
\(\mathcal{V}\)
为从
\(\Omega\)

\(\mathbb{R}\)
的连续函数空间,这些函数相对于某个固定的将
\(\Omega\)
划分为子区间的分区也是分段仿射的。注意到
\(\mathcal{V}\)
是一个有限维线性空间。设
\(d=\dim(\mathcal{V})\)
,令
\({\theta}\in \mathbb{R}^d\)
,并令
\(v^{\theta}\in \mathcal{V}\)
表示
\(\mathcal{V}\)
中的一个通用元素,参数化为
\({\theta}\)
。通过对
\(\mathcal{V}\)
中元素的积分得到的
CPAB
变换空间定义为

\[\begin{align}
&\mathcal{T}\triangleq
\Bigl \{
T^{\theta}:
x\mapsto \phi^{\theta}( x;1)
\text{ s.t. } \phi^{\theta}( x;t) \text{ solves the integral equation} \nonumber \\
& \phi^{\theta}(x;t) = x+\int_{0}^t v^{\theta}(\phi^{\theta}(x;\tau))\,
\mathrm{d}\tau \text{ where } v^{\theta}\in \mathcal{V}\,
\Bigr
\}\, .
\label{Eqn:IntegralEquation}
\end{align}
\]

可以证明,所有的
\(T^{\theta}\in\mathcal{T}\)
都是保序变换(即单调递增)并且是微分同胚。注意,尽管
\(v^{\theta}\in\mathcal{V}\)

CPA
,但
CPAB
变换
\(T^{\theta}\in\mathcal{T}\)
则不是(例如,
\(T^{\theta}\)
是可微的,这与任何非平凡的
CPA
函数不同)。公式
1
也意味着
\(\mathcal{V}\)
中的元素被视为速度场。

特别有用的事实有:

  1. \(\Omega\)
    的划分越精细,
    CPAB
    族的表现力就越强(这也意味着
    \(d\)
    增加)。
  2. CPAB
    变换使得在封闭形式中快速且准确地计算
    \(x\mapsto T^{\theta}(x)\)
    和梯度
    \(x\mapsto\nabla_{\theta} T^{\theta}(x)\)
    成为可能。

综上所述,这些事实意味着
CPAB
变换能提供了一种便捷且高效的方式来参数化和优化非线性单调递增函数。

The DiTAC Activation Function

论文提出的
TAF
称为
DiTAC
,是一种源自
CPAB
变换的
TAF

DiTAC
包含极少量的可训练参数,但它却具有很高的表现力。与现有的
TAF
不同的是,后者为每个输入通道专门分配一个参数,而
DiTAC
的表现力则来源于
CPAB
变换的表现力。

为了说明这一点,在图
3
中展示了在使用
ReLU

DiTAC
时,具有
3
个节点隐藏层的回归
MLP
中非线性是如何逐步演变的。在
ReLU
的情况下,表现力主要体现在对所有激活响应求和之后(并且结果函数在多个位置是不可微的),而
DiTAC
的表现力(和可微性)则在每个神经元经历的第一次数据变换中就明显体现出来。值得注意的是,
CPAB
变换及其梯度的封闭形式表达式的可用性使得
DiTAC
可以轻松地作为任何深度学习架构中任何激活函数的替代品。

现在解释
DiTAC
是如何构建的。回想一下,
CPAB
变换
\(T^{\theta}\)
是在有限区间
\([a,b]\)
上定义的。它的值域也是一个有限区间,这个区间可能与
\(\Omega\)
重合,也可能不重合(这取决于是否对
\(v^{\theta}\)
施加零边界条件)。由于某些激活函数的输入可能落在
\([a,b]\)
之外,主版本
DiTAC

\(T^{\theta}\)

GELU
结合在一起,后者是一些最先进模型中广泛使用的激活函数。回顾一下,
GELU
的定义为
\(\mathrm{GELU}(x) = x\cdot \Phi(x)\)
,其中
\(\Phi\)
是标准正态分布的累积分布函数。类似
GELU

DiTAC
函数是

\[\begin{align}
\mathrm{DiTAC}(x) = \tilde{x}\cdot\Phi(x)\,,
\qquad
\tilde{x} = \begin{cases}
T^{\theta}(x) & \quad \text{If } a \leq x \leq b\\
x & \text{Otherwise}\\
\end{cases}\,
\end{align}
\]

其中
\(T^{\theta}\)
是一个(可学习的)
CPAB
变换,而
\(\Omega=[a,b]\)

\(T^{\theta}\)
的定义域,由用户定义。这个主要的
DiTAC
版本是在后续的实验中使用的。

还可以通过将
\(T^{\theta}\)
与各种其他激活函数结合来构建其他版本的
DiTAC
,而不仅仅是与
GELU
结合。例如
Leaky-DiTAC
,其中
\(T^{\theta}\)
作用于
\([a,b]\)
,而其余的数据则通过
Leaky-ReLU

LReLU
)函数处理。也就是说,

\[\begin{align}
\mathrm{Leaky \ DiTAC}(x) =
\begin{cases}
T^{\theta}(x) & \quad \text{If } a \leq x \leq b\\ \mathrm{LReLU}(x) & \text{Otherwise}\\
\end{cases}
\end{align}
\]

有关这两种
DiTAC
类型的说明,请参见图
4

为了稳定训练并防止学习过于极端的变换,还要对速度场进行了正则化:

\[\begin{align}
\mathcal{L}_{\mathrm{reg}} = \sum\nolimits_{l=1}^{L} {{\theta}_l}^T \Sigma_{\mathrm{CPA}}^{-1} {\theta}_l
\end{align}
\]

其中,
\(L\)
是网络中激活层的数量,
\({\theta} \in {\mathbb{R}}^d\)

DiTAC
参数,
\(\Sigma_{\mathrm{CPA}}^{-1}\)
是与高斯平滑先验(在文献中提出)相关的
\(d \times d\)
协方差矩阵,用于
CPA
速度场。该矩阵有两个超参数:
\(\lambda_{var}\)
,用于控制速度场的方差,以及
\(\lambda_{smooth}\)
,用于控制不同子区间内速度的相似性,从而影响该场的平滑性(在机器学习的意义上)。

How to Drastically Reduce the Computational Cost

在深度学习(
DL
)中,训练通常涉及大量的激活函数(
AF
)调用。对于一个大小为
\((b,c,h,w)\)
的张量,其中
\(b\)
是批量大小,
\(c\)
是通道数量,
\((h,w)\)
是高度和宽度。对张量中的每个元素应用
CPAB
变换自然需要评估
\(b \cdot c \cdot h \cdot w\)
次。

例如,
ResNet-50
最后一个瓶颈块的
AF
在批量大小为
32
的情况下,操作约
800K
个元素。因此,尽管
CPAB
变换提供了表示微分同胚的高效解决方案,但在这里天真地使用这样的变换仍然可能在训练过程中产生显著的计算成本,并且过于缓慢。幸运的是,还有更好的方法。该方法能够在学习过程中显著减轻了这一成本。此外,在推理过程中,该解决方案使
DiTAC
与其他激活函数同样高效。

为了大幅降低学习过程中的成本,将区间
\([a,b]\)

CPAB
变换应用的区间)量化为
\(n\)
个离散值,且均匀分布。尽管会丢失一些信息,但在神经网络中,量化激活通常对准确性几乎没有影响,只要使用足够多的元素(通常
\(2^8\)
就足够了)。在这种方法中,对量化后的元素集使用
CPAB
变换,并创建一个查找表,然后可以用来转换输入张量中所有条目的值。即输出
\(y_i=T^{\theta}(Q(x_i))\)
,其中
\(Q(\cdot)\)
是量化函数,并且
\(Q(x_i)\in\{ a+k\Delta \}_{k=0}^n\)
,其中
\(\Delta=\tfrac{b-a}{n}\)

在反向传播中,采用了一种直通估计器的变体。仅计算量化值输出的
CPAB
导数,然后将其广播为
\(x_i\)
的导数估计:

\[\begin{align}
\frac{\partial T^{{\theta}}(x_i)}{\partial x_i} \approx \frac{\partial T^{{\theta}}(q_j)}{\partial q_j}, \enspace
\frac{\partial T^{{\theta}}(x_i)}{\partial \theta_\ell} \approx \frac{\partial T^{{\theta}}(q_j)}{\partial \theta_\ell}, \quad\text{where } q_j=Q(x_i).
\end{align}
\]

回顾
ResNet-50
的例子,只需对一个更小的条目集进行变换(例如,
\(2^{10}=1024\ll 800K\)
),就可以对相同的输入实现几乎相同的结果。在学习过程中,每当
\({\theta}\)
发生变化时(快速)构建这样的查找表。一旦学习完成并在推理之前,将计算一个单一的查找表(每个
DiTAC
函数一个),并在推理过程中根据需要重复使用该查找表。

DiTAC Versions


DiTAC
使用的
CPAB
变换
\(T^{\theta}\)
定义在一个有限区间上,即
\(\Omega=[a,b]\subset {\mathbb{R}}\)
,其共域也是一个有限区间。为了处理落在
\([a,b]\)
之外的输入数据,将
\(T^{\theta}\)

GELU
结合,
GELU
是一种在最新的先进模型中广泛使用的激活函数。通过将
\(T^{\theta}\)
与多种其他激活函数结合,或在
CPAB
的映射外定义某种函数(不一定是已知的激活函数),还可以构建
DiTAC
的其他版本。

需要注意的是,从概念上讲,可以通过首先应用一种将数据映射到
\(\Omega\)
的归一化方法,然后执行
CPAB
变换,最后将变换后的数据重新缩放回其原始范围,从而在整个输入数据上应用
CPAB
变换。然而,这样就必须提取整个输入数据的最小值和最大值,而这在训练过程中是很难实现的,因为这些值依赖于网络参数的学习。因此,在训练之前设置
\([a,b]\)
区间,通常包括大量的数据,并对超出该范围的数据应用不同的处理。

  • GELU-like DiTAC (DiTAC)

这是主要的
DiTAC
版本,也是所有实验中使用的版本。考虑到其在先进架构中的普遍性和成功,
GELU
是一个自然的选择。落在
\([a,b]\)
区间外的输入数据继承
GELU
的行为,而落在
\([a,b]\)
区间内的输入数据则首先经过
CPAB
变换,然后再通过
GELU
函数。

GELU-like DiTAC
定义如下:

\[\begin{align}
\mathrm{DiTAC}(x) = \tilde{x}\cdot\Phi(x)\,,
\qquad
\tilde{x} = \begin{cases}
T^{\theta}(x) & \quad \text{If } a \leq x \leq b\\
x & \text{Otherwise}\\
\end{cases}\,
\end{align}
\]

其中,
\(\Phi\)
是标准正态分布的累积分布函数(
CDF
),
\(T^{\theta}\)

CPAB
变换,
\(\Omega=[a,b]\)

\(T^{\theta}\)
的定义域,由用户定义。

  • GELU-DiTAC (GE-DiTAC)

这种激活函数类似于
DiTAC
的主要版本,只是这里仅对负输入值应用
GELU
,而对输入数据范围
\([0, b]\)
进行纯
CPAB
变换。为了保持函数的连续性(如果对
\(v^{\theta}\)
施加零边界条件),对大于
\(b\)
的值应用恒等函数。

GE-DiTAC
定义如下:

\[\begin{align}
\mathrm{GE-DiTAC}(x) = \begin{cases}
x\cdot\Phi(x) & \quad \text{If } x < 0 \\
T^{\theta}(x) & \quad \text{If } 0 \leq x \leq b\\
x & \quad \text{If } x > b\\
\end{cases}\,
\end{align}
\]

其中,
\(\Phi\)
是标准正态分布的累积分布函数(
CDF
),
\(T^{\theta}\)

CPAB
变换,
\(\Omega=[0,b]\)

\(T^{\theta}\)
的定义域,由用户定义。

需要注意的是,
GE-DiTAC
使
CPAB
变换的能力更加明显,因为这部分变换的数据并不与其他任何函数组合。从经验上看,在大多数实验中,它的表现与
DiTAC
相似,而它的优势主要在于使用简单网络进行简单回归任务时体现出来。

  • Leaky DiTAC (L-DiTAC)

这里
\(T^{\theta}\)
应用于
\([a,b]\)
区间,而其余数据通过
Leaky-ReLU
(
LReLU
)函数处理。也就是说,

\[\begin{align}
\mathrm{Leaky \ DiTAC}(x) =
\begin{cases}
T^{\theta}(x) & \quad \text{If } a \leq x \leq b\\ \mathrm{LReLU}(x) & \text{Otherwise}\\
\end{cases}
\end{align}
\]

其中,
\(T^{\theta}\)

CPAB
变换,
\(\Omega[a,b]\)

\(T^{\theta}\)
的定义域,由用户定义。这种版本可以被视为
ReLU
的一个更具表现力的版本。如所示,不同的激活函数(
AFs
)适合不同类型的数据和任务。这个
DiTAC
版本可能会改善在
ReLU
函数表现优于其他现有激活函数的问题。

  • Infinite-edges DiTAC (inf-DiTAC)

CPAB
变换是通过对
\(\mathcal{V}\)
中的元素进行积分而获得的,
\(\mathcal{V}\)
是一个从
\(\Omega\)

\({\mathbb{R}}\)
的连续函数空间,这些函数对于
\(\Omega\)
的某个固定划分为分段仿射的。在
inf-DiTAC
中,类似于
GE-DiTAC

L-DiTAC

\(T^{\theta}\)
应用于
\([a,b]\)
区间。对于落在该范围之外的输入数据,应用在
\(\Omega\)
的剖分(最右和最左单元)两侧学习到的仿射变换,从而产生一个完全由
CPAB
变换参数控制的连续激活函数。

inf-DiTAC
定义如下:

\[\begin{align}
\mathrm{inf-DiTAC}(x) =
\begin{cases}
A_{l}^{\theta} x & \quad \text{If } x < a \\
T^{\theta}(x) & \quad \text{If } a \leq x \leq b \\
A_{r}^{\theta} x & \quad \text{If } x > b \\
\end{cases}
\end{align}
\]

其中,
\(T^{\theta}\)

CPAB
变换,
\(A_{l}^{\theta}\)

\(A_{r}^{\theta}\)
分别是在剖分中最左和最右单元的仿射变换,而
\(\Omega[a,b]\)

\(T^{\theta}\)
的定义域,由用户定义。

在表
9
中,展示了所有上述
DiTAC
版本在论文中提出的二维函数重建任务上的性能评估。可以看出,
GE-DiTAC
在这个特定任务上提供了最佳性能。

Results




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

Graphql是什么?先来一段AI给的回答:
GraphQL是一种为API设计的查询语言,与REST相比,它提供了更高效、强大和灵活的方法来与数据交互。GraphQL由Facebook于2012年开发,并于2015年开源。其主要的优势在于能够允许客户端精确地指定他们需要的数据,从而避免了过度获取或数据不足的问题。
主要特性
  1. 精确获取需要的数据:
  1. 单一端点:
  1. 类型系统:
  1. 查询与修改:
  1. 实时数据(Subscription):
优势和局限
优势:
  • 减少数据传输:只返回客户端请求的数据。
  • 减少请求数:多个数据需求可以在单一查询中解决。
  • 灵活性高:客户端可以自由构造查询,无需服务器频繁更新API。
局限:
  • 复杂查询性能问题:如果不加限制地进行深度查询或大规模的数据嵌套,可能会对服务器性能造成影响。
  • 缓存策略:相比于REST的URL级别缓存,GraphQL需要更复杂的缓存策略来优化性能。
  • 学习曲线:对于开发者来说,需要学习新的查询语法及其底层实现。
其他内容就不过多介绍了,大家感兴趣可以自行去搜索有关理论或说明。接下来我直接提供实战入门演示。
以下开始正式演示正文:
先创建一个webapi项目作为服务端和一个控制台项目作为客户端,用来测试使用。以及对应的引用包,如下图所示:
0
新建Quries文件夹,用来存放查询使用的类和方法。以及新增一个测试用的类和string类型返回值的方法 Hello()
0
在启动项或Program里面,添加Graphql服务,并添加Query的类型注册:
0
最后还要记得映射端点:
0
然后运行程序,例如我默认运行起来端口是5264,则打开url(根据自己情况更改url地址):
http://localhost:5264/graphql/
然后输入查询语句:query:{hello}就可以查出对应的返回内容。
0
客户端里面,创建graphql的客户端请求,并输入查询的方法为hello的query语句,以及输出的结果,如下图所示。结果和上面的一样,只是我只输出data里面的数据,data里面的数据就是我们需要的结果。
0
接着做个拓展演示,创建一个嵌套实体类,用来模拟多种情况:
0
创建一个测试使用的服务,模拟具体查询业务使用。
0
注册服务和接口以后,运行程序,并在graphql里面进行运行测试。当前测试的是输出所有字段。
0
现在,例如我把子集合去掉不要,那查询出来也就不会带有子集合的任何内容:
0
或者只需要指定的其他字段,删掉了描述、子集合的城市字段:
0
同样的,把查询语句丢到客户端程序里面进行查询,也可以查出指定字段的内容:
0
上面演示的是查询效果,也可以做增删改等其他操作。
在测试服务类新增一个业务操作,模拟接收到参数以后进行了业务操作,最终返回一个代表成功的数据。例如:
0
新建一个Mutations文件夹,用来存放增删改操作的类等。例如此处的测试使用的TestMutation.然后创建一个模拟传入参数进行操作的方法,该方法返回上面服务类里面的测试方法。
0
需要添加对修改有关操作的注册:
0
然后启动,做个测试。使用mutation语句进行操作,操作指定方法,方法里面指定参数和字段数据。可以看到服务端进入了前面预设的业务方法内,并且返回的true被客户端成功接收。
0
在控制台客户端,也执行一下mutation操作,也能够成功调用:
0
以上是查询和修改操作的例子,graphql还可以做数据推送和订阅,用于实现websocket的效果。
新建一个subscriptions文件夹,用来存放所有的消息推送和订阅有关的定义类。例如TestSub,里面定义了一个推送方法OnTestPublish
0
在前面的测试服务里面,新增ITopicEventSender事件接口的注入,以及新增一个方法,用来触发推送功能。并且推送的主题,使用刚才定义的OnTestPublish
0
然后需要提供对推送服务的注册,以及持久化选择。
0
使用默认的持久化,该持久化选择不建议上生产。具体原因,我去AI一下:
  1. 可扩展性问题:AddInMemorySubscriptions 存储订阅信息是在内存中进行的。这意味着订阅数据仅存在于单个进程中。如果你的应用程序需要在多个服务器实例之间进行扩展,每个实例的内存中都会有独立的订阅状态,从而导致状态不一致。因此,在大型应用或高负载环境中,这种方法不能很好地扩展。
  2. 持久性缺失:使用内存存储的另一个主要问题是数据的持久性。服务器重启或发生故障时,所有在内存中的订阅数据将丢失。这对于生产环境来说是不可接受的,因为需要保证服务的稳定性和数据的持久性。
  3. 资源使用效率:随着订阅数量的增加,内存的使用量也会随之上升。在内存资源有限的环境中,这可能会影响应用程序的整体性能和响应速度。
  4. 故障恢复:在内存中的订阅管理缺乏有效的故障恢复机制。如果系统崩溃或需要进行维护,恢复订阅状态将非常困难,可能需要从客户端重新建立订阅。
为了解决这些问题,生产环境中通常建议使用持久化和可扩展的订阅存储方案,比如基于 Redis 的 AddRedisSubscriptions 方法。大佬们感兴趣可以自己去拓展下。
现在缺少一个触发条件,由于咱们创建的是webapi项目,自带控制器,那我把控制器做个改造,通过swagger来调用进行触发数据推送,直接在请求里面,调用推送方法:
0
最后,由于推送使用了websocket,所以也需要添加对websocket的注册:
0
然后启动程序,使用subscription进行订阅onTestPublish主题消息。运行以后,会一直监听,除非我们取消监听。
0
打开swagger,直接调用并测试,可以看到面板接收到了测试推送的数据。
0
客户端要实现订阅,需要做一些改动。订阅的事件是字符串类型,所以需要创建一个字符串类型的属性,用来接收数据:
0
然后客户端创建时候,需要使用websocket端点。然后再创建订阅语句
0
接下来是订阅的具体实现演示:
0
允许,并通过swagger调用两次测试,都可以被监听到。
0
同时,之前打开的graphql演示面板,也可以看到能够收到后续消息,说明支持多客户端接收,符合websocket的推送效果。
0
有关实现的核心代码。服务端注册有关:
//添加GraphQL服务
builder.Services
.AddGraphQLServer()
.AddQueryType
<TestQuery>()
.AddMutationType
<TestMutation>()
.AddSubscriptionType
<TestSub>()
.AddInMemorySubscriptions();
//默认消息持久化(生产情况建议更换)

var app =builder.Build();

app.UseWebSockets();
//映射GraphQL端点
app.MapGraphQL();

客户端实现:
var option = newGraphQLHttpClientOptions
{
EndPoint
= new Uri("http://localhost:5264/graphql"),//设置 WebSocket 端点以支持订阅
WebSocketEndPoint = new Uri("ws://localhost:5264/graphql")
};
using var client = new GraphQLHttpClient(/*"http://localhost:5264/graphql"*/ option , newNewtonsoftJsonSerializer());//var request = new GraphQLRequest//{//Query = @"mutation{//otherOperation(info:{address:""龙岗区宝龙街道"",city:""大大大深圳"",phone:""10100011""})//}"//};//var response = await client.SendQueryAsync<object>(request);//Console.WriteLine(response.Data);//定义订阅请求
var subscriptionRequest = newGraphQLRequest
{
Query
= @"subscription {
onTestPublish
}
"};//创建订阅流
var subscriptionStream = client.CreateSubscriptionStream<OnTestPublishResponse>(subscriptionRequest);//订阅消息流
var subscription =subscriptionStream.Subscribe(
response
=>{if (response.Errors != null)
{
Console.WriteLine(
"Error occurred:" +response.Errors);
}
else{
Console.WriteLine($
"Received message: {response.Data.OnTestPublish}");
}
},
error
=> Console.WriteLine($"Subscription error: {error.Message}"),
()
=> Console.WriteLine("Subscription completed."));//模拟其他逻辑(例如,在某个时刻取消订阅,这儿通过输入任意按键触发取消和释放)
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
//取消订阅并关闭 WebSocket 连接
subscription.Dispose();
client.Dispose();

如果需要我本地演示的代码项目,可以在本人公众号【
Dotnet Dancer
】回复:
代码演示
即可获取开源项目地址。如果需要其他咨询或合作,可V:WeskyNet001

如果以上内容对你有帮助,欢迎大佬们点赞、关注公众号或转发。谢谢大家!