分类 其它 下的文章

在现代 Web 开发中,Web 组件已经成为创建模块化、可复用 UI 组件的标准工具。而
Shadow DOM
是 Web 组件技术的核心部分,它允许开发人员封装组件的内部结构和样式,避免组件的样式和行为影响全局页面。然而,传统的 Shadow DOM 实现方式需要通过 JavaScript 显式地创建和附加 Shadow DOM,这增加了开发复杂性。

为了简化 Web 组件开发,
声明式 Shadow DOM(Declarative Shadow DOM)
提供了一种新的方法,允许开发人员直接通过 HTML 定义 Shadow DOM,而无需过多依赖 JavaScript。这一特性特别适用于服务端渲染(SSR)和静态页面生成(SSG)场景,大大提高了页面的加载效率和开发体验。

本文将详细介绍声明式 Shadow DOM 的基础语法、与 Javascript 的结合使用以及其主要应用场景和优势。


一、什么是 Shadow DOM?

Shadow DOM 是 Web 组件的一个重要组成部分,它通过创建封装的 DOM 树,让组件的内部 DOM 和样式与外部页面隔离。这使得组件可以拥有独立的样式和功能,而不会与页面的其他部分发生冲突。

传统上,开发人员需要通过 JavaScript 调用
attachShadow()
方法来手动创建 Shadow DOM,并附加到自定义元素上。这样的方式增加了代码的复杂性,同时在服务端渲染和静态页面生成中也难以直接使用。

二、声明式 Shadow DOM 的基本语法

声明式 Shadow DOM 允许开发人员直接在 HTML 模板中定义 Shadow DOM,而无需通过 JavaScript 来创建。这种方式依赖于 HTML 中的
<template>
标签,并通过
shadowroot
属性来指定 DOM 应作为 Shadow DOM 存在。

示例代码:

<my-element>
  <template shadowrootmode="open">
    <style>
      p {
        color: blue;
      }
    </style>
    <p>这是声明式 Shadow DOM 内的内容!</p>
  </template>
</my-element>

在这个例子中,
<template>
标签用于定义组件的内部结构和样式,而
shadowrootmode="open"
表示这是一个开放的 Shadow DOM,可以从外部访问。

相比传统的创建方式,这种声明式的语法更加简洁,也更利于服务器端预渲染。

三、声明式 Shadow DOM 与 Javascript 结合

虽然声明式 Shadow DOM 允许在 HTML 中直接声明组件结构,但自定义元素的行为和逻辑仍然需要通过 Javascript 来定义。例如,如果需要为组件添加交互行为,我们仍然需要编写 JavaScript 代码来注册自定义元素。

示例:声明式 Shadow DOM + Javascript 实现计数按钮

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>声明式 Shadow DOM 示例</title>
</head>

<body>
    <!-- 定义组件的模板 -->
    <count-button>
        <template shadowrootmode="open">
            <style>
                button {
                    font-size: 16px;
                    padding: 10px 20px;
                }
            </style>
            <button id="increment-btn">点击次数:<span id="count">0</span></button>
        </template>
    </count-button>
    <script>
        // 定义自定义元素类
        class CountButton extends HTMLElement {
            constructor() {
                super();

                // 获取按钮和计数显示元素
                this.button = this.shadowRoot.querySelector('#increment-btn');
                this.countDisplay = this.shadowRoot.querySelector('#count');
                this.count = 0; // 初始化计数

                // 绑定事件处理程序
                this.button.addEventListener('click', () => {
                    this.increment();
                });
            }

            // 定义一个方法来增加计数
            increment() {
                this.count++;
                this.countDisplay.textContent = this.count;
            }
        }

        // 注册自定义元素
        customElements.define('count-button', CountButton);
    </script>

</body>

</html>

预览

jcode

代码解释:

  1. HTML 部分


    • 使用
      <template>
      标签定义了计数按钮组件的结构和样式,并通过
      shadowrootmode="open"
      声明为开放的 Shadow DOM。
    • 组件的样式和内容在 HTML 中声明,减少了 Javascript 中的 DOM 操作。
  2. Javascript 部分


    • 使用 Javascript 定义了一个自定义元素
      CountButton
    • 添加了按钮点击事件,每次点击按钮时,计数器加一并更新显示。
  3. 自定义元素注册


    • 使用
      customElements.define
      方法注册了自定义元素
      <count-button>

四、声明式 Shadow DOM 的应用场景

1. 服务端渲染(SSR)

声明式 Shadow DOM 对服务端渲染非常友好。由于组件结构和样式已经声明在 HTML 中,服务端可以预先生成完整的组件,并将其直接发送给客户端。这不仅减少了页面的初始加载时间,还提高了搜索引擎的抓取能力,有利于 SEO。

2. 静态页面生成(SSG)

在静态页面生成中,声明式 Shadow DOM 允许开发人员将预定义的组件结构嵌入到静态 HTML 文件中,从而提升页面的加载速度,减少客户端的 Javascript 计算量。

五、声明式 Shadow DOM 的优势与限制

优势:

  • 简化开发流程
    :通过 HTML 直接声明 Shadow DOM,减少了对 Javascript 的依赖,降低了开发难度。
  • 性能提升
    :在 SSR 和 SSG 场景下,预渲染的组件可以直接发送给客户端,减少了首次渲染的时间。
  • SEO 友好
    :组件内容可以直接包含在 HTML 中,便于搜索引擎抓取。

限制:

  • Javascript 仍不可或缺
    :虽然组件的结构和样式可以声明式定义,但组件的交互和逻辑仍需通过 JavaScript 实现。
  • 浏览器兼容性
    :目前声明式 Shadow DOM 已基本支持所有的浏览器,但是所需的浏览器的版本较新,需要开发者考虑兼容性问题。


六、总结

声明式 Shadow DOM
是 Web 组件开发的一项强大新功能,它通过简化 Shadow DOM 的创建过程,减少了 Javascript 的依赖,特别适用于服务端渲染和静态页面生成场景。虽然其优势明显,但在实际开发中,开发者仍需结合 Javascript 来实现组件的交互和逻辑。

随着浏览器对这一新特性的支持逐步增加,声明式 Shadow DOM 将会成为 Web 组件开发中的主流方式之一。对于需要高性能、模块化的 Web 开发项目,声明式 Shadow DOM 是一个值得尝试的新工具。

参考资料:

数据库容灾等级


在信息化时代,企业的数据安全和业务连续性变得至关重要,容灾备份作为确保数据不丢失和业务不中断的重要措施备受关注。

我们通常将容灾备份分为四个等级,从最基本的本地备份到复杂的异地多活系统,每个等级的特点和适用场景各不相同。

下面我们就来详细了解一下这四个等级的容灾备份方案。

容灾备份容灾等级

1、第0级:没有备份数据中心
这一级容灾备份,实际上没有灾难恢复能力,它只在本地进行数据备份,并且备份的数据只在本地保存,没有送往异地。
描述:一般新业务、即将下架的业务、边缘业务会使用这个等级,所以数据库选型上要求也不高,一般单机MySQL,SQL Server等都能支撑。


2、第1级:本地磁带备份,异地保存
在本地将关键数据备份,然后送到异地保存。当灾难发送后,按预定数据恢复程序进行恢复。这种方案成本低、易于配置。但当数据量增大时,存在存储介质难以管理的问题;
并且当灾难发生时,存在大量数据难以及时恢复的问题。为了解决这些问题,当灾难发生时,可先恢复关键数据,后恢复非关键数据。
描述:当新业务到一定规模,会考虑使用这个等级,使用更高性价比的存储介质存储数据库备份,例如磁带库,并且备份保留相当长的时间,需要人工或者通过网络定期把存储介质存放异地保存。



3、第2级:热备份站点备份
在异地建立一个热备份站点,通过网络进行数据备份。也就是通过网络以同步或异步方式,把主站点的数据备份到备份站点。备份站点一般只备份数据,不承担业务。
当出现灾难时,备份站点接替主站点业务,从而维护业务运行的连续性。
描述:这个等级一般是业务进入稳定期,需要考虑把容灾等级提升一个档次,这时候数据库选型上,一般需要具备跨机房数据同步能力,例如,SQL Server的AlwaysOn、MySQL的MGR、PostgreSQL的流复制等能满足;
如果是用公有云,那么RDS数据库的多可用区就能满足要求。



4、第3级:活动备份中心
在相隔较远的地方分别建立两个数据中心,它们都处于工作状态,并相互进行数据备份。当某个数据中心发生灾难时,另一个数据中心接替其工作任务。
这种级别的备份根据实际要求和投入资金的多少可以分为两种:
(1)两个数据中心之间只限于关键数据的相互备份
(2)两个数据中心之间互为镜像,即零数据丢失
零数据丢失是目前要求最高的一种容灾备份方案,它要求不管发生什么灾难,系统都能保证数据安全。所以它需要配置复杂的管理软件和专用硬件设备,相对而言投资是最高的,但恢复速度是最快的。
描述:一般金融业务等对数据安全要求比较高的需要达到这个等级,也就是我们常说的,异地双活、异地多活,数据库选型上一般需要具备分布式多节点数据同步能力,例如,某Base,某SQL等能满足。

不同的容灾备份对应的灾难恢复能力

建设成本和灾难恢复目标时间对比


总结

本文介绍了数据库容灾备份的四个等级,从本地备份(第0级)到复杂的异地双活系统(第3级),每个等级适用于不同的业务场景。
文章重点分析了各级备份方案的特点和应用,帮助企业根据RTO、RPO等需求选择合适的方案,以确保数据安全和业务连续性。

其实从第0级到第3级,本质上都是为了满足更高要求的RTO和RPO,满足更苛刻的SLA,所以在数据库选型和方案选择上都要结合实际,选出最适合的方案。

参考文章
https://news.west.cn/39450.html
https://e.huawei.com/cn/industries/commercial-market/active-active-data-center-solution
https://stor.zol.com.cn/374/3741281.html
https://blog.csdn.net/hjx020/article/details/106588133/

本文版权归作者所有,未经作者同意不得转载。

1 关于云原生

云原生计算基金会(Cloud Native Computing Foundation, CNCF)的官方描述是:
云原生是一类技术的统称,通过云原生技术,我们可以构建出更易于弹性扩展、极具分布式优势的应用程序。
这些应用可以被运行在不同的环境当中,比如说 私有云、公有云、混合云、还有多云场景。
云原生包含了 容器、微服务(涵盖服务网格)、Serverless、DevOps,API管理、不可变基础架构等能力。通过云原生技术构建出来的应用程序,对
底层基础架构的耦合很低,易于迁移,可以充分地利用云所提供的能力,因此云原生应用的研发、部署、管理相对于传统的应用程序更加高效和便捷。

image

1.1 微服务

微服务是一种架构模式,是面向服务的体系结构(SOA)软件架构模式的一种演变,
它提倡将单一应用程序划分成一组松散耦合的细粒度小型服务,辅助轻量级的协议,互相协调、互相配合,为用户提供最终价值。
具有 单一职责、轻量级通信、独立性、进程隔离、混合技术栈和混合部署方式、简化治理 等特点。

1.2 DevOps

DevOps 作为一种工程模式,本质上是通过对开发、运维、测试,配管等角色职责的分工,实现工程效率最大化,进而满足业务的需求。

1.3 持续交付

在不影响用户使用服务的前提下频繁把新功能发布给用户使用,要做到这点比较难。需要强大的流量管理能力,动态的服务扩缩容为平滑发布、ABTesting提供保障。

1.4 容器化

容器化的好处在于运维的时候不需要再关心每个服务所使用的技术栈了,每个服务都被无差别地封装在容器里,可以被无差别地管理和维护,现在比较流行的技术是docker和k8s。

2 关于ServiceMesh

2.1 什么是ServiceMesh

ServiceMesh 是最新一代的微服务架构,作为一个基础设施层,能够与业务解耦,主要解决复杂网络拓扑下微服务与微服务之间的通信,其实现形态一般为轻量级网络代理,并与应用SideCar部署,同时对业务应用透明。
image
如果从一个单独链路调用可以得到以下的结构图:
image
如果我们从一个全局视角来看,绿色的为应用服务,蓝色的为SideCar,就会得到如下部署图:
image

2.2 相较传统微服务的区别

以SpringCloud与Dubbo为代表的微服务开发框架非常受欢迎。但我们发现,他有优秀的服务治理能力,也有明显的痛点:
1. 侵入性强。
想要集成SDK的能力,除了需要添加相关依赖,业务层中入侵的代码、注解、配置,与治理层界限不清晰。可以想想Dubbo、SpringCloud 等的做法
2. 升级成本高。
每次升级都需要业务应用修改SDK版本,重新进行功能回归测试,并对每一台服务进行部署上线,与快速迭代开发相悖。
3. 版本碎片化严重。
由于升级成本高,而中间件版本更新快,导致线上不同服务引用的SDK版本不统一、能力参差不齐,造成很难统一治理。
4. 中间件演变困难。
由于版本碎片化严重,导致中间件向前演进的过程中就需要在代码中兼容各种各样的老版本逻辑,带着"枷锁”前行,无法实现快速迭代。
5. 内容多、门槛高。
依赖组件多,学习成本高。
6. 治理功能不全。
不同于RPC框架,SpringCloud作为治理全家桶的典型,也不是万能的,诸如协议转换支持、多重授权机制、动态请求路由、故障注入、灰度发布等高级功能并没有覆盖到。

2.3 ServiceMesh的价值 — 赋能基础架构

  1. 统一解决多语言框架问题,降低开发成本
  2. 降低测试成本,提升质量
  3. 控制逻辑集中到控制面
  4. 为新架构演进提供支持,如Serverless
  5. 网格半覆盖 转 统一覆盖(弥补service-center并逐渐过度)
  6. 完整的闭环微服务统筹和管理能力

image

2.4 ServiceMesh的价值 — 赋能业务

  • 框架与业务解耦,减少业务限制。
  • 简化服务所依赖SDK版本管理。
  • 依托热升级能力,版本召回周期短。
  • SDK瘦身,减少业务依赖冲突。
  • 丰富的流量治理、安全策略、分布式Trace、日志监控,下沉服务治理底座,让业务专注业务。

image

3 ServiceMesh 核心能力

3.1 流量治理

微服务应用最大的痛点就是处理服务间的通信,而这一问题的核心其实就是流量管理。

3.1.1 请求路由

将请求路由到服务的版本,应用根据 HTTP 请求 header 的值、Uri的值 路由流量到不同的地方。匹配规则可以是流量端口、header字段、URI等内容。
RuleMatch参考
image

3.1.2 流量转移

当微服务的一个版本逐步迁移到另一个版本时,我们可以将流量从旧版本迁移到新版本。如下图,使用weight参数进行权重分配,
这个很典型的应用场景就是灰度发布或者ABTesting。
image

3.1.3 负载均衡

同3.1.2的图,Service B 有多个实例,所以可以另外制定负载均衡策略。
负载均衡策略支持简单的负载策略(ROUND_ROBIN、LEAST_CONN、RANDOM、PASSTHROUGH)、一致性 Hash 策略和区域性负载均衡策略。

3.1.4 超时

对上游的请求设置,设置一个一定时长(0.5s)的超时,请求超过这个时间不响应,可以直接fallback。目标还是过载保护。
image

3.1.5 重试

当请求在固定的时间内没有返回正确值的时候,可以配置重试次数。设置如果服务在 1 秒内没有返回正确的返回值,就进行重试,重试的条件为返回码为5xx,重试 3 次。
分布式环境下,重试是高可用的重要技术,重试方案慎用。

retries:
      attempts: 3
      perTryTimeout: 1s
      retryOn: 5xx

3.1.6 熔断/限流/降级

熔断的策略比较多,可以配置 最大连接数、连接超时时间、最大请求数、请求重试次数、请求超时时间等,我们都可以给他熔断掉,fallback回去。
但是目前看,Istio 对更灵活、更细粒度的限流、降级等能力支持的还不够好,合理应该有漏斗池算法(如
阿里开源限流框架Sentinel
)或者令牌桶算法(如
Google Guava 提供的限流工具类 RateLimiter
)这样的灵活做法。
但是可以采用其他方式处理,比如可以通过流量转发将部分流量流动到默认服务去,该服务启用默认的fallback,但是需要控制好采样时间、熔断半开的策略。

3.1.7 离群检测(Outlier Detection)

当集群中的服务故障的时候,其实我们最优先的做法是先进行离群,然后再检查问题,处理问题并恢复故障。所以,能否快速的离群对系统的可用性很重要。
Outlier Detection 允许你对上游的服务进行扫描,然后根据你设置的参数来判断是否对服务进行离群。
下面的配置表示每秒钟扫描一次上游主机,连续失败 2 次返回 5xx 错误码的所有主机会被移出负载均衡连接池 3 分钟,上游被离群的主机在集群中占比不应该超过10%。
但无论比例多少,只要你集群下的服务实例>=2个,都将弹出至少1个主机。它有很详细的配置,
参考这边

注意:3分钟之后回群,如果再被离群,则为上次离群时间+本次离群时间,即 3+3;默认超过50%(可调整比例)被离群,进入恐慌模式。

outlierDetection:
      consecutiveErrors: 2
      interval: 1s
      baseEjectionTime: 3m
      maxEjectionPercent: 10

3.1.8 故障注入

就是用来模拟上游服务对请求返回指定异常码时,当前的服务是否具备处理能力。系统上线前,可以配置注入的httpStatus和比例,来验证下游服务对故障的处理能力。
image

3.1.9 流量镜像(Mirroring)

这个也叫做影子流量。是指通过一定的配置将线上的真实流量复制一份到镜像服务中去,可以设置流量比例,只转发不响应。
个人觉得这个还是比较有用的,好处是 完整的线上正式环境模拟、流量分析、压力测试;全真的线上问题再现,方便问题排查。
image

3.2 可观察性

3.2.1 监控与可视化

Prometheus(标配,默认抓取指标数据)、kiali监控(服务视图,Istion链路的可观察性) 、Grafana(BI报表)(数据面、控制面、xDS Service 各健康指标)
后续章节会逐一展开...

3.2.2 访问日志

ELK、EFK (Envoy记录AccessLog,包含SideCard的InBound、OutBound记录)
后续章节会详细展开...

3.2.3 分布式追踪

本质上查找多个HTTP请求之间的相关性的一种方法是使用相关性ID。该ID应该传递给所有请求,以便跟踪平台知道哪些请求属于同一请求。如下图:
image
尽管Istio利用Envoy的分布式跟踪功能提供开箱即用的跟踪集成,但是其实这是一个误解,我们的应用程序需要做一些工作。应用程序需要传播以下header:

  • x-request-id
  • x-b3-traceid
  • x-b3-spanid
  • x-b3-parentspanid
  • x-b3-sampled
  • x-b3-flags
  • x-ot-span-context

image
Istio Sidecar内的Envoy代理接收这些标头,并将它们传递给配置的tracing系统。所以实际上,Istio中服务追踪默认只会追踪到2级,
例如A -> B -> C, 在Istio中会出现2条追踪链路:A -> B 和B -> C,而不会出现我们期望的A -> B -> C的形式,如果想要服务串联起来,需要对服务间调用进行改造,
在Istio中应用程序通过传播http header来将span关联到同一个trace。

image

3.3 安全机制

  • Service Mesh可以在服务间通信中引入双向TLS加密,确保数据在传输过程中不被篡改和窃听。控制平面负责管理和分发证书,Sidecar Proxy在通信过程中进行加密和解密操作。
  • 通过引入身份认证和访问控制策略,可以细粒度地控制哪些服务可以访问其他服务。

3.4 策略执行

Service Mesh通过在每个服务实例旁边部署Sidecar Proxy,实现了对服务间通信的透明代理。这些代理负责拦截出入的所有流量,并根据控制平面下发的配置和策略执行相应的操作。具体工作原理如下:

3.4.1 服务发现:

当一个服务实例启动时,它会向服务注册中心注册自己的信息。控制平面负责管理这些服务实例信息,并将更新的服务列表分发给所有Sidecar Proxy。

3.4.2 流量管理:

当一个服务需要与另一个服务通信时,流量首先经过本地的Sidecar Proxy。代理根据配置的路由规则和负载均衡策略,将流量转发到目标服务实例。
控制平面可以动态更新这些路由规则,实现蓝绿部署、金丝雀发布等高级流量管理功能。

3.4.3 安全认证:

Service Mesh可以在服务间通信中引入双向TLS加密,确保数据在传输过程中不被篡改和窃听。控制平面负责管理和分发证书,Sidecar Proxy在通信过程中进行加密和解密操作。
通过引入身份认证和访问控制策略,可以细粒度地控制哪些服务可以访问其他服务。

3.4.4 可观察性:

Service Mesh中的代理会收集每个请求的日志、监控数据和追踪信息,并将这些数据发送到可观察性组件进行处理和存储。
运维人员可以通过控制平面提供的接口和仪表盘,实时监控服务间的流量情况、延迟、错误率等指标,并进行故障排查和性能优化。

4 总结

Service Mesh相比传统微服务框架以下几方面有明显优势:

  • 解耦应用程序和通信逻辑
  • 提供增强的服务治理能力
  • 提高可观察性和可调试性
  • 支持多语言和协议以
  • 提高系统可靠性和可扩展性

经过前面的Redis基础学习,今天正式进入编码阶段了,进入编码阶段我们又同样面临一道多选题,选择什么客户端库?要是有选择困难症的又要头疼了。不过别担心我先头疼,今天就给大家介绍6款.NET系Redis客户端库: ServiceStack.Redis、StackExchange.Redis、CSRedisCore、FreeRedis、NewLife.Redis、BeetleX.Redis。

01
、ServiceStack.Redis

ServiceStack.Redis算的上最老牌、最有名的一款Redis C#/.NET客户端库了,但是因为商业性导致对于大多数人来说不是首选。

ServiceStack.Redis是一款功能丰富、操作简单、高性能的C#/.NET客户端库,对原生的功能和特性提供很好的支持,同时又做了更高级的功能抽象,使得对简单对象或复杂类型序列化操作更容易。当然也同时提供了同步和异步API。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"ServiceStack.Redis 使用示例");
    //创建连接池
    var pool = new RedisManagerPool("127.0.0.1:6379");
    //获取一个redis实例
    using var redis = pool.GetClient();
    //设置键值对
    var setResult = redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.Get<string>("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Remove("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.ContainsKey("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

02
、StackExchange.Redis

StackExchange.Redis是一款基于.NET的、高性能的、免费的、功能全面的、通用的老牌Redis客户端。并且支持Redis多节点,Redis集群,IO多路复用,同步/异步双编程模型等技术,这也使得其与Redis交互同时兼具灵活性与高效性,大大提升了Redis读写的性能与并发。

同时它还提供了丰富的高级功能,包括但不限于管道,连接池,事务,Lua脚本、订阅/发布等。序列化与压缩也提供了多种方式供以选择,很方便与.NET应用程序集成。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"StackExchange.Redis 使用示例");
    // 创建 ConnectionMultiplexer 实例
    using var connection = ConnectionMultiplexer.Connect("127.0.0.1:6379");
    //获取 Redis 数据库实例
    var redis = connection.GetDatabase();
    //设置键值对
    var setResult = redis.StringSet("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.StringGet("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.KeyDelete("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.KeyExists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

03
、CSRedisCore

CSRedisCore是一款国人基于开源项目csredis上实现的著名Redis C#/.NET客户端库。它做到了所有方法名和redis-cli方法名保持一致。它支持Redis 集群、Redis 哨兵和Redis主从分离,以及geo类型、流类型命令,同时支持同步/异步接口。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"CSRedisRedis 使用示例");
    // 创建 CSRedisClient 实例
    var redis = new CSRedisClient("127.0.0.1:6379");
    //设置键值对
    var setResult = redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.Get("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Del("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.Exists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

04
、FreeRedis

FreeRedis是CSRedisCore作者的另一个大作。至少从逻辑上来说也应该比CSRedisCore更优秀,事实也是如此,FreeRedis在内存使用、存储效率都做了优化,在持久化、容错方面也做了改进,同时还提供了更多的高级功能以及自定义选项。我们直接看官方介绍。

单从介绍上来说CSRedisCore有的功能它有,CSRedisCore没有的功能它也有。总的来说功能更强大了。另外CSRedisCore目前处于维护阶段已经不新增功能了。因此更推荐FreeRedis。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"FreeRedis 使用示例");
    // 创建 CSRedisClient 实例
    var redis = new RedisClient("127.0.0.1:6379");
    //设置键值对
    redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作成功");
    //获取键对应的值
    var value = redis.Get("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Del("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.Exists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

05
、NewLife.Redis

NewLife.Redis具有低延时,高性能,高吞吐量以及稳定性、可靠性良好,因此在大量实时数据计算的应用场景有很好的发挥。它为针对大数据和消息队列做了优化,使得其可以用支撑日均百亿级的调用量,而它的连接池可以做到100000个连接并发。在包含网络通讯的前提下可以把get/set操作做到平均耗时200~600微秒。其二进制序列化方式也更有助于提升数据存储和读取效率。

下面我们写个简单的使用小例子:

public static void Run()
{
    Console.WriteLine($"NewLife.Redis 使用示例");
    // 创建 CSRedisClient 实例
    var redis =  new FullRedis("127.0.0.1:6379", "", 0);
    //设置键值对
    var setResult = redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = redis.Get<string>("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = redis.Remove("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = redis.ContainsKey("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

06
、BeetleX.Redis。

BeetleX.Redis是一款高可用、高性能、异步非阻塞设计的.net core客户端库。并且基本全面覆盖redis-cli指令,提供了多种序列化方式,使用简单轻松。

下面我们写个简单的使用小例子:

public static async Task RunAsync()
{
    Console.WriteLine($"BeetleX.Redis 使用示例");
    // 创建 CSRedisClient 实例
    RedisDB redis = new RedisDB(0)
    {
        DataFormater = new JsonFormater()
    };
    //添加写主机
    redis.Host.AddWriteHost("127.0.0.1", 6379);
    //添加读主机
    redis.Host.AddReadHost("127.0.0.1", 6379);
    //设置键值对
    var setResult = await redis.Set("key1", "value1");
    Console.WriteLine($"设置键值对key1/value1操作结果:{setResult}");
    //获取键对应的值
    var value = await redis.Get<string>("key1");
    Console.WriteLine($"获取键key1对应的值为:{value}");
    // 删除键
    var delResult = await redis.Del("key1");
    Console.WriteLine($"删除键key1操作结果:{delResult}");
    //检查键是否存在
    var exists = await redis.Exists("key1");
    Console.WriteLine($"键key1是否存在: {exists}");
}

执行结果如下:

07
、总结

  • ServiceStack.Redis:综合功能全面,适合需要商业支持的用户。

  • StackExchange.Redis:官方推荐,功能全面,社区支持良好,文档丰富。

  • CSRedisCore:功能齐全,简单易用,适合快速开发。

  • FreeRedis:高性能,功能齐全,简单易用,适合快速开发。

  • NewLife.Redis:高性能,高并发,低延迟,分布式场景适合使用。

  • BeetleX.Redis。:高可用,高性能,异步操作,适合高负载场景。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

论文阅读翻译之Deep reinforcement learning from human preferences

关于

  • 首次发表日期:2024-09-11
  • 论文原文链接:
    https://arxiv.org/abs/1706.03741
  • 论文arxiv首次提交日期:12 Jun 2017
  • 使用KIMI,豆包和ChatGPT等机翻,然后人工润色
  • 如有错误,请不吝指出

Deep reinforcement learning from human preferences(基于人类偏好的深度强化学习)

Abstract (摘要)

对于复杂的强化学习(RL)系统来说,要与现实世界环境有效互动,我们需要向这些系统传达复杂目标。在这项工作中,我们探索了以(非专家)人类对轨迹段对的偏好来定义目标。我们展示了这种方法可以在没有奖励函数的情况下有效解决复杂的RL任务,包括Atari游戏和模拟机器人运动,同时仅需对不到1%的代理与环境交互提供反馈。这大大降低了人类监督的成本,使其能够实际应用于最先进的强化学习系统。为了展示我们方法的灵活性,我们表明可以在大约一小时的人类参与时间内成功训练出复杂的新行为。这些行为和环境比以往任何从人类反馈中学到的都要复杂得多。

1 Introduction (引言)

最近在将强化学习 (RL) 扩展到大规模问题上取得的成功,主要得益于那些具有明确奖励函数的领域(Mnih等, 2015, 2016; Silver等, 2016)。不幸的是,许多任务的目标是复杂的、定义不清的或难以明确说明的。克服这一限制将大大扩展深度强化学习的潜在影响,并可能进一步扩大机器学习的应用范围。

例如,假设我们想使用强化学习训练一个机器人来清洁桌子或炒鸡蛋。如何构建一个合适的奖励函数并不明确,而这个奖励函数需要依赖机器人的传感器数据。我们可以尝试设计一个简单的奖励函数,大致捕捉预期的行为(intended behavior),但这通常会导致机器人行为优化我们的奖励函数,但机器人行为并实际上却不符合我们的偏好。这种困难是构成近期关于我们价值观(values)与强化学习系统目标不一致的基础(Bostrom, 2014; Russell, 2016; Amodei等, 2016)。如果我们能够成功地向智能体(agent)传达我们的实际目标,将是解决这些问题的关键一步。

如果我们拥有所需任务的示范,就可以通过逆向强化学习 (Ng 和 Russell, 2000) 提取一个奖励函数,然后使用该奖励函数来训练通过强化学习训练一个智能体。更直接的方式是使用模仿学习(imitation learning)来复制示范的行为。然而,这些方法并不适用于人类难以演示的行为(例如控制一个具有多自由度且形态与人类差异很大的机器人)。

另一种方法是允许人类对系统当前的行为提供反馈,并利用这些反馈来定义任务。原则上,这符合强化学习的范式,但直接将人类反馈作为奖励函数对于需要数百或数千小时经验的强化学习系统来说成本过高。为了能够在实际上基于人类反馈训练深度强化学习系统,我们需要将所需反馈的量减少几个数量级。

我们的方法是从人类反馈中学习奖励函数,然后优化这个奖励函数。这种基本方法之前已经被考虑过,但我们面对的是如何将其扩展到现代深度强化学习中的挑战,并展示了迄今为止从人类反馈中学到的最复杂的行为。

总之,我们希望找到一个解决方案来处理没有明确指定奖励函数的顺序决策问题,这个解决方案应该满足以下条件:

  1. 能够解决我们只能
    识别
    期望行为但不一定能够示范的任务,
  2. 允许非专家用户教导智能体,
  3. 能够扩展到大型问题,且
  4. 在用户反馈方面经济高效。

我们的算法在训练策略优化当前预测的奖励函数的同时,根据人类的偏好拟合一个奖励函数(见图1)。我们要求人类比较智能体行为的短视频片段,而不是提供绝对的数值评分。我们发现,在某些领域,进行比较对人类来说更容易,同时在学习人类偏好时同样有效。比较短视频片段的速度几乎与比较单个状态一样快,但我们证明,这种比较方式显著更有帮助。此外,我们还表明,在线收集反馈能够提高系统性能,并防止它利用所学奖励函数的漏洞。

我们的实验在两个领域进行:Arcade Learning Environment(Bellemare等, 2013)中的Atari游戏,以及物理模拟器MuJoCo(Todorov等, 2012)中的机器人任务。我们展示了即使是非专家人类提供的少量反馈,从十五分钟到五小时不等,也足以学习大多数原始的强化学习任务,即使奖励函数不可观察。随后我们在每个领域中考虑了一些新行为,例如完成后空翻或按照交通流向驾驶。我们证明了我们的算法能够通过大约一小时的反馈学习这些行为——即使很难通过手工设计奖励函数来激励这些行为。

大量研究探索了基于人类评分或排序的强化学习,包括 Akrour 等 (2011)、Pilarski 等 (2011)、Akrour 等 (2012)、Wilson 等 (2012)、Sugiyama 等 (2012)、Wirth 和 Fürnkranz (2013)、Daniel 等 (2015)、El Asri 等 (2016)、Wang 等 (2016) 和 Wirth 等 (2016)。另一些研究则关注从偏好而非绝对奖励值出发的强化学习问题 (Fürnkranz 等, 2012; Akrour 等, 2014),以及在非强化学习环境中通过人类偏好进行优化的研究 (Machwe 和 Parmee, 2006; Secretan 等, 2008; Brochu 等, 2010; Sørensen 等, 2016)。

我们的算法遵循与Akrour等人(2012)和Akrour等人(2014)相同的基本方法。他们研究了四个自由度的连续域和小的离散域,在这些域中,他们可以假设奖励在手编码特征的期望中是线性的。我们则研究具有几十个自由度的物理任务和没有手工设计特征的 Atari 任务;我们环境的复杂性迫使我们使用不同的强化学习算法和奖励模型,并应对不同的算法权衡。一个显著的区别在于,Akrour等人(2012)和Akrour等人(2014)是从整个轨迹中获取偏好,而不是短片段。因此,虽然我们收集了多两个数量级的比较,但我们的实验所需的人类时间少于一个数量级。其他区别主要在于调整我们的训练程序,以应对非线性奖励模型和现代深度强化学习,例如使用异步训练和集成方法。

我们对反馈引导的方法与 Wilson 等人 (2012) 的研究非常接近。然而,Wilson 等人 (2012) 假设奖励函数是到某个未知“目标”策略的距离(该策略本身是手工编码特征的线性函数)。他们通过贝叶斯推理拟合这个奖励函数,而不是执行强化学习,他们根据目标策略的最大后验估计 (MAP) 生成轨迹。他们的实验涉及的是从其贝叶斯模型中抽取的“合成”人类反馈,而我们进行了从非专家用户收集反馈的实验。目前尚不清楚 Wilson 等人 (2012) 的方法是否可以扩展到复杂任务,或是否能够处理真实的人类反馈。

MacGlashan 等 (2017)、Pilarski 等 (2011)、Knox 和 Stone (2009)、以及 Knox (2012) 进行了一些涉及基于真实人类反馈的强化学习实验,尽管他们的算法方法并不十分相似。在 MacGlashan 等 (2017) 和 Pilarski 等 (2011) 的研究中,学习仅在人工训练者提供反馈的回合(episodes)中进行。这在像 Atari 游戏这样的领域似乎是不可行的,因为学习高质量策略需要数千小时的经验,即使对于我们考虑的最简单任务,这种方法的成本也过于昂贵。TAMER(Knox, 2012; Knox 和 Stone, 2013)也学习奖励函数,但他们考虑的是更简单的设置(settings),在这些设置中,期望的策略可以相对快速地学习。

我们的工作也可以看作是合作逆向强化学习框架( cooperative inverse reinforcement learning framework)(Hadfield-Menell 等, 2016)的一个特定实例。这个框架考虑了一个人类和机器人在环境中互动的两人游戏,目的是最大化人类的奖励函数。在我们的设置中,人类只能通过表达他们的偏好来与这个游戏进行互动。

与之前的所有工作相比,我们的关键贡献是将人类反馈扩展到深度强化学习,并学习更复杂的行为。这符合将奖励学习方法扩展到大型深度学习系统的最新趋势,例如逆强化学习(Finn等人,2016年)、模仿学习(Ho和Ermon,2016年;Stadie等人,2017年)、半监督技能泛化(Finn等人,2017年)以及从示范中引导强化学习(Silver等人,2016年;Hester等人,2017年)。

2 Preliminaries and Method(预备知识与方法)

2.1 Setting and Goal(配置与目标)

我们考虑一个智能体在一系列步骤中与环境进行交互;在每个时刻
\(t\)
,智能体从环境中接收观察
\(o_t \in \mathcal{O}\)
,然后向环境发送动作
\(a_t \in \mathcal{A}\)

在传统的强化学习中,环境还会提供奖励
\(r_t \in \mathbb{R}\)
,智能体的目标是最大化奖励的折扣和(discounted sum of rewards)。与假设环境生成奖励信号不同,我们假设有一位人类监督者可以在
轨迹片段
(trajectory segments)之间表达偏好。轨迹片段是观察和动作的序列,
\(\sigma=\left(\left(o_0, a_0\right),\left(o_1, a_1\right), \ldots,\left(o_{k-1}, a_{k-1}\right)\right) \in(\mathcal{O} \times \mathcal{A})^k\)
。我们用
\(\sigma^1 \succ \sigma^2\)
表示人类更偏好轨迹片段
\(\sigma^1\)
而非轨迹片段
\(\sigma^2\)
。非正式地说,智能体的目标是生成人类偏好的轨迹,同时尽量减少向人类询问的次数。

更确切地说,我们将通过两种方式评估我们算法的行为:

定量:
我们说偏好
\(\succ\)
是由一个奖励函数
[1]
\(r: \mathcal{O} \times \mathcal{A} \rightarrow \mathbb{R}\)
生成的,如果

\[\left(\left(o_0^1, a_0^1\right), \ldots,\left(o_{k-1}^1, a_{k-1}^1\right)\right) \succ\left(\left(o_0^2, a_0^2\right), \ldots,\left(o_{k-1}^2, a_{k-1}^2\right)\right)
\]

每当

\[r\left(o_0^1, a_0^1\right)+\cdots+r\left(o_{k-1}^1, a_{k-1}^1\right)>r\left(o_0^2, a_0^2\right)+\cdots+r\left(o_{k-1}^2, a_{k-1}^2\right)
\]

如果人类的偏好是由奖励函数
\(r\)
生成的,那么我们的智能体应当根据
\(r\)
获得高的总奖励。因此,如果我们知道奖励函数
\(r\)
,我们就能对代理进行量化评估。理想情况下,代理应达到的奖励几乎与其使用强化学习来优化
\(r\)
时一样高。

定性
:有时我们没有奖励函数来对行为进行定量评估(这正是我们的方法在实际中有用的情况)。在这些情况下,我们只能定性地评估智能体满足人类偏好的程度。在本文中,我们将从一个用自然语言表达的目标开始,要求人类根据智能体实现该目标的情况来评估智能体的行为,然后展示智能体尝试实现该目标的视频。

我们的基于轨迹片段比较的模型与 Wilson 等人 (2012) 中使用的轨迹偏好查询非常相似,不同之处在于我们不假设可以将系统重置为任意状态
[2]
,并且我们的片段通常从不同的状态开始。这使得人类比较的解释(interpretation of human comparisons)变得更加复杂,但我们展示了即使人类评分者对我们的算法不了解,我们的算法也能够克服这一难题。

2.2 Our Method(我们的方法)

在每个时刻,我们的方法维持一个策略
\(\pi: \mathcal{O} \rightarrow \mathcal{A}\)
和一个奖励函数估计
\(\hat{r}: \mathcal{O} \times \mathcal{A} \rightarrow \mathbb{R}\)
,它们均由深度神经网络参数化。

这些网络通过三个过程进行更新:

  1. 策略
    \(\pi\)
    与环境交互,生成一组轨迹
    \(\left\{\tau^1, \ldots, \tau^i\right\}\)
    。使用传统的强化学习算法更新
    \(\pi\)
    的参数,以最大化预测奖励的总和
    \(r_t=\hat{r}\left(o_t, a_t\right)\)
  2. 从步骤1生成的轨迹
    \(\left\{\tau^1, \ldots, \tau^i\right\}\)
    中选择片段对
    \(\left(\sigma^1, \sigma^2\right)\)
    ,并将它们发送给人类进行比较。
  3. 通过监督学习优化映射
    \(\hat{r}\)
    的参数,以拟合迄今为止从人类收集的比较结果。

2.2.1 Optimizing the Policy (对策略进行优化)

在使用
\(\hat{r}\)
计算奖励后,我们面临的是一个传统的强化学习问题。我们可以使用任何适合该领域的强化学习算法来解决这个问题。一个细微之处在于,奖励函数
\(\hat{r}\)
可能是非平稳的(non-stationary),这使我们倾向于选择对奖励函数变化具有鲁棒性的算法。这导致我们专注于策略梯度方法(policy gradient methods),这些方法已经成功应用于这类问题(Ho 和 Ermon, 2016)。

在本文中,我们使用
优势演员-评论员(advantage actor-critic)
(A2C;Mnih 等, 2016)来玩 Atari 游戏,并使用
信赖域策略优化(trust region policy optimization)
(TRPO;Schulman 等, 2015)来执行模拟机器人任务。在每种情况下,我们都使用了被发现对传统强化学习任务有效的参数设置。我们唯一调整的超参数是 TRPO 的熵奖励(entropy bonus),因为 TRPO 依赖信赖域来确保足够的探索,如果奖励函数不断变化,这可能导致探索不足。

我们将
\(\hat{r}\)
生成的奖励归一化(normalized)为均值为零、标准差恒定。这是一个典型的预处理步骤,尤其适合于我们的学习问题,因为奖励的位置(position of the rewards)在我们的学习过程中是未定的。

2.2.2 Preference Elicitation(偏好获取)

人类监督者会看到两个可视化的轨迹片段,以短视频片段的形式呈现。在我们所有的实验中,这些视频片段的时长在 1 到 2 秒之间。

然后,人类指示他们更喜欢哪个片段,或者表示两个片段同样优秀,或者表示他们无法比较这两个片段。

人类的判断记录在数据库
\(\mathcal{D}\)
中,形式为三元组
\(\left(\sigma^1, \sigma^2, \mu\right)\)
,其中
\(\sigma^1\)

\(\sigma^2\)
是两个片段,
\(\mu\)
是一个在
\(\{1,2\}\)
上的分布,表示用户更喜欢哪个片段。如果人类选择一个片段为更优,则
\(\mu\)
将所有权重放在该选择上。如果人类标记这两个片段为同样可取,则
\(\mu\)
是均匀分布。最后,如果人类标记这两个片段不可比较,则该比较将不包含在数据库中。

2.2.3 Fitting the Reward Function (拟合奖励函数)

我们可以将奖励函数估计
\(\hat{r}\)
视为一个偏好预测器,如果我们将
\(\hat{r}\)
看作解释人类判断的潜在因素,并假设人类选择偏好片段
\(\sigma^i\)
的概率呈指数地取决于在片段长度上潜在奖励的合计值:
[3]

\[\hat{P}\left[\sigma^1 \succ \sigma^2\right]=\frac{\exp \sum \hat{r}\left(o_t^1, a_t^1\right)}{\exp \sum \hat{r}\left(o_t^1, a_t^1\right)+\exp \sum \hat{r}\left(o_t^2, a_t^2\right)}
\tag{1}
\]

我们选择
\(\hat{r}\)
以最小化这些预测与实际人类标签之间的交叉熵损失:

\[\operatorname{loss}(\hat{r})=-\sum_{\left(\sigma^1, \sigma^2, \mu\right) \in \mathcal{D}} \mu(1) \log \hat{P}\left[\sigma^1 \succ \sigma^2\right]+\mu(2) \log \hat{P}\left[\sigma^2 \succ \sigma^1\right]
\]

这遵循了从成对偏好估计评分函数Bradley-Terry模型(Bradley和Terry,1952),并且是Luce-Shephard选择规则(Luce,2005;Shepard,1957)在轨迹片段上的偏好的特化。它可以理解为将奖励等同于一个偏好排序尺度(preference ranking scale),类似于为国际象棋开发的著名的 Elo 排名系统(Elo,1978)。就像两个国际象棋棋手的 Elo 分数之差估计了一个棋手在一盘国际象棋比赛中击败另一个棋手的概率一样,两个轨迹片段的预测奖励之差估计了人类选择一个而不是另一个的概率。

我们实际的算法对这个基本方法进行了一些修改,早期实验发现这些修改很有帮助,并在第3.3节中进行了分析:

  • 我们拟合一个预测器的集合(ensemble),每个预测器都是在从
    \(\mathcal{D}\)
    中抽样的
    \(|\mathcal{D}|\)
    个三元组上训练的(允许重复抽样)。估计值
    \(\hat{r}\)
    通过独立地对每个预测器进行归一化,然后对结果取平均来定义。
  • 数据中有
    \(1/e\)
    的部分被保留,作为每个预测器的验证集。我们使用
    \(\ell_2\)
    正则化,并调整正则化系数,以保持验证损失在训练损失的1.1到1.5倍之间。在某些领域,我们还应用 dropout 进行正则化。
  • 我们不是像公式 1 中描述的那样直接应用 softmax,而是假设人类有 10%的概率随机均匀地(uniformly)做出响应。概念上,这种调整是必要的,因为人类评估者有一个固定的犯错误概率,这个概率不会随着奖励差异变得极端而衰减至0。

2.2.4 Selecting Queries (选择查询)

我们根据奖励函数估计器的不确定性近似来决定如何查询偏好,这类似于Daniel等人(2014)的方法:我们采样大量的长度为
\(k\)
的轨迹片段对,使用我们集合中的每个奖励预测器来预测每一对中哪个片段会被偏好,然后选择那些在集合成员之间预测方差最高的轨迹。这是一种粗糙的近似,第三节中的消融实验表明,在某些任务中它实际上损害了性能。理想情况下,我们希望基于查询的信息价值来查询(Akrour等人, 2012; Krueger等人, 2016),但我们留待未来的工作进一步探索这一方向。


  1. 在这里,我们假设奖励是观察和动作的函数。而在我们的 Atari 环境实验中,我们假设奖励是前四次观察的函数。在一般的部分可观测环境中,我们可以考虑依赖于整个观察序列的奖励函数,并使用递归神经网络来建模此奖励函数。
    ↩︎

  2. Wilson 等人 (2012) 还假设可以采样合理的初始状态。然而,我们处理的是高维状态空间,在这种情况下随机状态可能无法达到,而预期的策略(intended policy)位于一个低维流形上。
    ↩︎

  3. 公式1没有使用折扣因(discounting),这可以被解释为建模人类对于轨迹片段中事件发生的时间是无所谓的。使用显式的折扣因子或推断人类的折扣函数也是合理的选择。
    ↩︎