wenmo8 发布的文章

以目前的时代来说,微前端并不算是一个很新的概念了,微前端的本质就是大型应用的拆分与关联。在我刚开始学微前端的时候,就接触到了如下的概念:比如基座式微应用、自组织式微应用,或者微前端的实现方案比如:路由分发、iframe、应用微服务化、微件化、微应用化等等。如果你刚开始接触微前端的概念,想要学习微前端,这些概念一定会让你感到困惑,甚至有一种无从下手的感觉。本篇内容,就是为了解决这些问题而存在的。希望可以分享一些我在学习微前端过程中的一些感悟和思路。

一、微前端的本质

其实要说起微前端的本质很简单,就是分类。当某一个“东西”体积、量级开始变得庞大之后,以“某种逻辑”进行分类成了几乎必然的选择。而天下大事,必然是分久必合,合久必分的动态螺旋的状态的。

而驱动微前端这个“东西”分类的原因,则在于前端范畴下的用户体验要求和技术能力的提升,二者相辅相成,一方面时代的发展,底层硬件能力的提升,保证了Web软件的飞速发展,同时用户对于新产品的界面、交互、体验等,也随之提高。

而基于这样的背景,前后端分离之后,前端项目的体积也愈发庞大。这就导致不得不考虑如何去拆分前端应用以保持良好的项目体验的问题了。好在,后端早就经历了类似的阶段,于是从微服务的概念中衍生出了微前端的概念。

而无论是微服务也好,还是微前端也罢,只不过是不同技术领域,通过某些技术手段来分割代码的一种思路。

二、微前端技术要点

无论我们是在学习某一门技术,或者在应用某一个框架,或者我们在玩一个RPG游戏,往往都有两件事情尤其需要我们去关注:主线与要点。

拿《塞尔达传说:荒野之息》作为一个例子,有很多的主线任务去做,林克最终的目的是去打倒灾厄盖侬,拯救塞尔达公主。而除了主线之外,还有很多其他的游玩内容,比如神庙挑战,比如支线任务,比如地图探索等等。
我们还可以以Vue3的学习作为例子,我们首先要了解的就是Vue3的生命周期,整个Vue3的运行时经历了哪些步骤和核心环节,然后我们再去学一些它的关键API。
换句话说,“点”与“线”才最终组成了一件事情的完整面貌。通过“点”与“线”的链接,形成了我们的知识体系,也就是“面”。

1.微前端拆分思路

当我初次涉足微前端领域时,一系列专业术语立刻吸引了我的注意:iframe方案、路由式微前端、微件化、微应用化、NPM方案、动态Script方案、Module Federation等等。这些术语如同微前端世界的关键词,每一个都代表了一种实现微前端的具体技术手段。
面对这些技术点,我最初的困惑是:这些技术点虽然具体,但它们能否构成一个完整的技术方案呢?是否能够作为理论层面的指导呢?我带着这些疑问,开始深入探索微前端的本质。在我的理解中,这些技术点更像是实现微前端的表面应用,而非深层次的理论指导。
直到后来我学到了这样的概念:横向拆分与纵向拆分。

1.)横向拆分

我们先来看下下面的图片。
我们从图中可以看到,整个页面由三部分构成,分别是页头页脚、轮播图和官网详情展示,这是一个很常见的官网页面。但是它却是由三个团队协作开发完成并将输出结果组装在一个页面上的。这种方法提供了很大的灵活性,给我们在不同的视图中复用微前端提供了可能。
在我个人的理解中,横向拆分更倾向于模块化,特别是在面向消费者(ToC)的项目中应用。试想一下,像腾讯视频、爱奇艺这样的视频播放网站,它们的核心功能,比如视频播放和视频展示,通常都具备功能完善、内容丰富、多页面复用等特性。这些特点使得它们非常适合通过横向拆分的方式,实现微前端架构的优化。
换个角度来看,实际上横向拆分的概念,在第一章节中我就已经提前提及了,那就是模块化。我们可以将一个或多个核心功能视为一个模块,这样它们就可以灵活地应用到整个网站需要的各个部分。
横向拆分特别注重业务功能的复用性以及网站整体的搜索引擎优化(SEO),因为对于面向消费者(ToC)的项目来说,SEO是一个必然的需求。
由于需要组合多个模块,并且要对网站的SEO进行技术优化,同时还要明确划分不同团队的责任界限,横向拆分在技术实施和团队协作方面,无疑面临着更加严峻的挑战。

2)纵向拆分

我们还是先看一下图。
图中展示了两个页面,它们各自承担着不同的业务职责。我们可以看到,一个是产品详情页,另一个是产品创建页。尽管它们的业务功能不同,但它们的头部导航和侧边导航部分是共用的。这种由一个团队负责整个页面的设计和开发的方式,被称为纵向拆分。在这种情境下,如何合理地拆分业务,可以借助领域驱动设计(Domain-Driven Design, DDD)作为指导思想。
如果你经常写单页应用,纵向拆分其实要比横向拆分更容易理解,也更容易实施。
不知道大家还记不记得这样一句话,叫做前端在一定程度上是天然解耦的。什么意思呢?就是往往,我们在开发分配任务的时候,很少会说,你写一个搜索框,他写一个列表,然后最后组合在一起,而是你写这个页面,他写那个页面,最终在项目合并的时候也不会互相干扰和依赖。而这种场景以及拆分方式,其实更适合ToB的后台管理系统、SaaS类项目。
这样的项目往往业务领域明确,范围清晰,但是业务逻辑和场景则多变和复杂。通过纵向拆分可以聚合针对某一领域理解深刻的开发及产品团队,专注于单一领域,为整个项目的迭代和发展带来更大的优势。

2.限界上下文

关于界限上下文,以及如何区分核心子域、支持性子域、通用子域这样的概念我并不想这里过多的讨论,这本书毕竟不是聊领域驱动设计的,如果大家有兴趣,完全可以自行学习,我更想和大家聊一聊的是,限界上下文这个概念,在微前端领域处在一个什么位置,有什么作用。

限界上下文按照我的理解,其实就是要按照什么样的逻辑来拆分微前端,比如说,我们可以按照业务范畴来拆分。在一个通常的SaaS系统里,可以拆分为应收应付、订单创建、产品、仓储等等子系统。而在一个ToC的网购软件或者视频网站上,则可以按照其复用性或功能性来拆分,比如购物车、产品列表、产品详情,或者视频播放器、视频详情等等。
但是很多时候,可能有些理论并不是那么有用,举个例子,现在有一个老旧的SaaS项目,整个目录结构和业务领域压根不搭边,全都是平铺出去的,但是现在领导希望可以应用新的技术,把老旧的项目也整合进来,进而开发新的业务,达到项目上的大一统。还记不记得之前我们聊过微前端的使用场景,增量升级那个,现在我们就要来做这样一件事。
那怎么办呢?如果我拆分老旧的系统,区分其所归属的业务领域,会花费我们大量的时间来分离子系统,本来老旧的项目很稳定了,也无需什么更改,结果你要是这么一搞,不知道要给测试、开发带来多少问题,所以,我们希望老旧系统不动,直接用Nginx反向代理或者iframe就好了。没错,这是一个很好的选择,但是同时带来的问题就是,你遵循了领域驱动设计了么?很显然并没有,但是这却是必要且正确的错误选择。
上面举得这个例子想要告诉大家的就是,理论是指导实践的必要工具,但是很多时候,我们未必一定要遵循理论。

3. 组合

组合的概念,实际上指的是我们要如何拼凑及加载微前端界面,比如我们熟知的qiankun、wujie等微前端框架,都属于客户端组合的范畴。除了客户端组合,还有服务器端组合以及边缘侧组合。
我们先来了解一下这样一个流程,当我们在浏览器中看到界面的时候,在这之前都发生了什么。很简单,就是从服务器获取界面所需的资源,比如html、js、css这些,然后经过浏览器的解析渲染,最终呈现出界面。但是为了更快的获取资源,在客户端和服务器中间,往往可能还有一个CDN用来存储前端的静态资源,从而加快获取速度。
那么我们再来理解一下关于组合的三种方式:
  • 客户端组合:其实就是通过js在客户端运行时加载微前端及其相关功能,所有的事情都是在客户端完成的。
  • 边缘侧组合:我们会在CDN层对视图进行组合,通过一种叫做ESI的类似于XML的标记语言可以达成这样的目的。
  • 服务器组合:其实说白了,就是类似于SSR,在服务器的运行时或者编译时进行组合,拼凑微前端从而生成最终的视图结果后,返回给前端完整的HTML,这样做,最大的优点是提升客户端的体验以及提供良好的SEO效果。

4. 路由

在现代单页应用如此广泛普及的情况下,想必大家对于客户端路由一点都不陌生了。在大多数情况下,我们选择怎样的微前端组合方式,就会对应的使用怎样的微前端路由方式。当然,这种情况不是绝对的,就比如假设我们选择服务器组合,但是服务器要承受的压力太大,就可以把路由分发的事情交给CDN来做,也就是边缘侧路由。
这里我要稍微强调一下, CDN路由或者CDN组合是属于边缘侧的一种方案,但是边缘侧并不仅仅只有CDN一种。边缘计算是一种分布式计算范例,它将计算资源和数据存储接近最终用户的位置,以便提供更低的延迟和更高的性能。换句话说,为了增强客户端或者减轻服务器压力的一些中间或额外的基础设备所造成的增强性能力,都可以算作边缘侧。
无论是哪种方法,在实际操作中,我们并不局限于单一的选择。也就是说,我们既可以在客户端进行组件的组合,也可以在服务器端进行部分组合。同时,我们既可以实现客户端路由,也可以针对某些特定的路径,直接从服务器端请求数据或者通过内容分发网络(CDN)获取数据。这种灵活性使得我们能够根据项目的具体需求和特点,选择最合适的实现方式。

5. 通信

从微前端的定义上来讲,其实我们并不需要子系统之间的通信,因为微前端的定义就是独立自治,它不应该和其它子系统产生任何理论上的通信和关系。但是我也多次强调,理论很多时候并不是那么好用,我们也无需教条于理论。
虽然微前端的定义如此独立,但是微前端之间的通信在大多数场景下都是十分必要的。比如登录状态的共享,子系统间信息的传递等等。
如果你是在同域的情况下,完全可以使用WebStorage的SessionStorage、LocalStorage或者cookie来进行登录状态的共享。哪怕你跨域了我们也可以使用postMessage进行通信。
但是,假设,你跨域了一个并非自主开发的项目,也就是你想要接入别人的跨域的项目,还无法获取源码或者私有部署,那,你还需要通信么?
给大家开了一个小小的脑洞,我们继续哈。除了以上的方案外,我们也可以利用现成的框架如wujie、qiankun等来进行现成的开箱即用的通信手段。
当然,除了这些我们也可以自己实现一个简易的EventBus来进行数据的共享。还有一种我们常用的选择,也就是通过URL的query来进行通信,这种方案简单实用,几乎没有什么技术难度,但是如果你要想做一个通用的URL传递参数的方法,在微前端中进行实践,也还是要多思考一些的。
最后,我们来总结下,在微前端中进行通信都有哪些可行的方案。
  1. WebStorage
  2. Cookie
  3. PostMessage
  4. EventBus
  5. 自定义事件,即发布订阅模式
  6. URL
  7. 其它:比如window.name等。
  8. Vuex、Redux等状态管理工具。

6. 隔离

隔离这个话题并非是随着微前端概念的出现才诞生的,它一直存在,并且一直困扰着开发者们。我们都希望拥有一个干净、不受干扰的环境来发挥我们的技术才能。然而,JavaScript变量可能会被后来的代码覆盖,CSS选择器的使用可能无意中降低了其他样式的优先级,这样的问题确实令人头疼。
于是js从IIFE发展到规范化的ES Module,css也在某些特定的领域拥有了自己的scope。那么在微前端的范畴下,要如何应对js和css的隔离问题呢?
在原生的背景下,我们都是基于iframe或者webComponent作为微前端实践的选型,从而从根本的角度上进行js和css的隔离,再去解决因为选型所带来的某些副作用,这种方案就是wujie微前端框架的隔离解决方案。
当然,除了原生方案,我们还可以使用如BEM等命名空间的CSS命名方式来隔离CSS、通过shadowDOM来原生隔离CSS和HTML。
关于JS的隔离,方案其实有很多,比如Webpack Module Federation、各种微前端框架等等,但是他们实现的核心,仍旧离不开基本的原理。就拿JS来说,你到了运行时的环境,要隔离的话,无非就是模块化或者IIFE等基本的JS隔离方案。

三、微前端方案浅析

微前端本质上来说是一个技术架构思想,基于我们前两章对于微前端的优缺点、微前端的拆分方案等的了解,我们对微前端已经有了一个具体一些的认识。我们了解了微前端的优缺点,微前端的适用场景与实现要点等等。但是这些内容只能算是形而上的理论或者原则,它的意义是指导具体的实践,而在具体的实践中,我们往往需要一些具体的方案或者手册来指导。
本章,我们就来依据市面上一些微前端方案,去实现一些对应方案的简单例子,深入的去理解理论与实际的交界是什么样的。
目前来说,微前端的实现方案大体有如下几种:
  1. Router(路由式)
  2. Iframe(前端容器化)
  3. Web Component(应用组件化)
  4. 微服务化
  5. 微件化(组件式)
  6. 微应用化(组合式)
前三种我们很好理解,就是基于某种Web核心能力作为支持,来实现我们的微前端方案,通过这些能力再配备一些技术我们就可以做到不错的微前端体验。但是后三种方案,并没有说明要使用哪种技术点作为实践的核心能力,而更像是一种针对某一个“能力”的描述,即模糊,又不具体,需要我们花费一些时间去了解它们的区别。
在我们不了解某一个知识点并且没有太好的切入点时,我们可以尝试从它的名称作为切入点。
微服务化听起来似乎与服务器密切相关,其核心确实与后端的微服务紧密相连。关键在于实现“完全的独立性”,即从开发到部署,再到构建和运行,每个服务都是完全独立的,与其他应用没有直接的联系。最终,通过“模块化”的方式将这些独立服务组合成一个完整的应用集合。微服务化的最终目标是提高整个系统的可维护性和扩展性,同时减少开发团队之间的耦合。要牢记的关键点是“独立”和“降低耦合”。通过这种方式,每个团队可以专注于自己的服务,而不必过多担心其他服务的影响,从而提高开发效率和系统的灵活性。
而微件化从名称上来说,核心在于“微件”。微件(widget),指的是一段可以直接嵌入在应用上运行的代码,它由开发人员预先编译好,在加载时不需要再做任何修改或者编译。换句话说,微件就是一段已经完备的随时可以拿来使用的一段代码,这段代码可以以任何形式存在,目的是“拿来即用”。
微应用化则是指把业务或者需求拆分成一个有一个独立的应用,在构建时可以组合成一个完整的业务应用。因为要有页面上代码的组合,所以这些微应用需要依赖于相同的框架环境,没办法混合多个框架。

四、微前端之道

上面我们只是简单的介绍了一下微前端相关的基础理论概念,限于篇幅,不再赘述。

如果大家有兴趣学习微前端相关的知识,大家可以在文末的链接,在京东购买我新出版的关于微前端相关的偏入门方面的《微前端之道》这本书。

本书只有极少的必要的概念讲解,大约百分之二十左右(前两章),从第三章起,就会开始实现各种微前端方案的简单例子。

后面的章节会分为两个部分,第一部分会带大家一起实现一个完整Saas项目,其中包括了从开发到部署服务器的所有步骤,包括Jenkins、Nginx、Docker的简单使用等等,打破大家对服务器陌生的壁垒。

然后会带大家实现微前端的NPM方案、动态Script方案,以及现代各种微前端框架的简单使用等等。

这本书理论和实践结合,没有通篇枯燥的理论,没有脱离实际,也没有晦涩难懂的深度,这本书很适合作为微前端的入门读物。

目前市面上的微前端书籍并不多,我个人觉得唯一有价值的就是《微前端实战》,所以在结合个人经验和学习的路径,最后写了这本跟微前端有关的书籍。希望可以给大家带来一些必要的帮助。

京东链接:
https://item.jd.com/14348331.html

当当网:
https://product.dangdang.com/29823513.html

我们在项目开发中,面对一些高并发、大数据量等业务场景,往往对SQL语句的性能要求比较高,这个时候为了方便灵活控制,我们一般就会编写原生的SQL。

Dapper就是一个非常高性能的轻量级ORM框架,Dapper采用原生SQL语句的方式,对于CRUD这些简单操作我们一般都会自行封装,不然就会变成很繁琐。

今天给大家推荐一个Dapper的CRUD扩展库,就可以满足我们的需求,减少我们的工作量。


01
项目简介

Dapper.SimpleCRUD是一个开源项目,它基于Dapper开发的,为开发者提供了简单的CRUD操作帮助器。

Dapper本身是一个轻量级的ORM框架,它允许开发者使用SQL语句直接操作数据库,同时又能享受到ORM带来的便利,如参数化查询等。

而Dapper.SimpleCRUD则进一步简化了这一过程,让开发者能够更轻松地执行CRUD操作。

Dapper.SimpleCRUD的核心功能

CRUD操作:通过简单的API,开发者可以轻松地执行插入、读取、更新和删除操作,包括Get、GetList、Insert、Update、Delete等。通过扩展IDbConnection接口,可以直接使用,无需额外的配置。

模型属性支持:通过使用属性(如[Key]、[Table]、[Column]等),可以轻松地指定模型与数据库表之间的映射关系。同时提供[Editable(false)]、[ReadOnly(true)]、[IgnoreSelect]、[IgnoreInsert]、[IgnoreUpdate]等属性,用于控制模型属性在CRUD操作中的行为。

异步操作支持:对于需要处理大量数据或需要提高响应速度的应用,Dapper.SimpleCRUD提供了异步操作的支持。

02
使用方法

1、定义模型

[Table("Users")]  
public class User  
{  
    [Key]  
    public int UserId { get; set; }  
    public string FirstName { get; set; }  
    public string LastName { get; set; }  
    public int Age { get; set; }  

    // 额外的、不在数据库中的属性  
    [Editable(false)]  
    public string FullName { get { return string.Format("{0} {1}", FirstName, LastName); } }  
}

2、执行CRUD操作

// 假设已经有一个打开的数据库连接 connection  

// 插入操作  
var newId = connection.Insert(new User { FirstName = "John", LastName = "Doe", Age = 30 });  

// 读取操作  
var user = connection.Get<User>(newId);  

// 更新操作  
user.Age = 31;  
connection.Update(user);  

// 删除操作  
connection.Delete<User>(newId);


03
项目地址

https://github.com/ericdc1/Dapper.SimpleCRUD

更多开源项目:
https://github.com/bianchenglequ/NetCodeTop

- End -

推荐阅读

2个零基础入门框架教程!

Html2OpenXml:HTML转化为OpenXml的.Net库,轻松实现Html转为Word。

SharpLab:.Net反编译工具,方便实时查看反编译后的代码!

盘点4个.Net跨平台图形开源库!

Flurl:一个Star 3.9K的链式RESTful风格HTTP开源.Net库

前言

本文主要介绍tensorboard的使用。
tensorboard是一个可视化的,支持人工智能学习的一个工具。
tensorboard的官方地址:
https://www.tensorflow.org/tensorboard
本文内容来自视频教程16课,个人感觉对于tensorboard讲的非常好。

Tensorboard的使用

使用代码如下:

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt 
import sys
import torch.nn.functional as F
from torch.utils.tensorboard import SummaryWriter

# pip install tensorboard 安装 tensorboard
# 启动 tensorboard 启动成功的话,地址是http://localhost:6006/
# logdir要等于 SummaryWriter('runs/mnist1')的入参地址
# tensorboard --logdir=C:\Project\python_test\github\PythonTest\PythonTest\PythonTest\pytorchTutorial\runs
# tensorboard的官方地址:https://www.tensorflow.org/tensorboard

############## TENSORBOARD ########################
writer = SummaryWriter('runs/mnist1')
###################################################

# Device configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# Hyper-parameters 
input_size = 784 # 28x28
hidden_size = 500 
num_classes = 10
num_epochs = 1
batch_size = 64
learning_rate = 0.001

# MNIST dataset 
train_dataset = torchvision.datasets.MNIST(root='./data', 
                                           train=True, 
                                           transform=transforms.ToTensor(),  
                                           download=True)

test_dataset = torchvision.datasets.MNIST(root='./data', 
                                          train=False, 
                                          transform=transforms.ToTensor())

# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, 
                                           batch_size=batch_size, 
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, 
                                          batch_size=batch_size, 
                                          shuffle=False)

examples = iter(test_loader)
example_data, example_targets = next(examples)

for i in range(6):
    plt.subplot(2,3,i+1)
    plt.imshow(example_data[i][0], cmap='gray')
#plt.show()

############## TENSORBOARD ########################
img_grid = torchvision.utils.make_grid(example_data)
writer.add_image('mnist_images', img_grid)
#writer.close()
#sys.exit()
###################################################

# Fully connected neural network with one hidden layer
class NeuralNet(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(NeuralNet, self).__init__()
        self.input_size = input_size
        self.l1 = nn.Linear(input_size, hidden_size) 
        self.relu = nn.ReLU()
        self.l2 = nn.Linear(hidden_size, num_classes)  
    
    def forward(self, x):
        out = self.l1(x)
        out = self.relu(out)
        out = self.l2(out)
        # no activation and no softmax at the end
        return out

model = NeuralNet(input_size, hidden_size, num_classes).to(device)

# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  

############## TENSORBOARD ########################
writer.add_graph(model, example_data.reshape(-1, 28*28).to(device))
#writer.close()
#sys.exit()
###################################################

# Train the model
running_loss = 0.0
running_correct = 0
n_total_steps = len(train_loader)
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # origin shape: [100, 1, 28, 28]
        # resized: [100, 784]
        images = images.reshape(-1, 28*28).to(device)
        labels = labels.to(device)
        
        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()

        _, predicted = torch.max(outputs.data, 1)
        running_correct += (predicted == labels).sum().item()
        if (i+1) % 100 == 0:
            print (f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{n_total_steps}], Loss: {loss.item():.4f}')
            ############## TENSORBOARD ########################
            writer.add_scalar('training loss', running_loss / 100, epoch * n_total_steps + i)
            running_accuracy = running_correct / 100 / predicted.size(0)
            writer.add_scalar('accuracy', running_accuracy, epoch * n_total_steps + i)
            running_correct = 0
            running_loss = 0.0
            ###################################################

# Test the model
# In test phase, we don't need to compute gradients (for memory efficiency)
class_labels = []
class_preds = []
with torch.no_grad():
    n_correct = 0
    n_samples = 0
    for images, labels in test_loader:
        images = images.reshape(-1, 28*28).to(device)
        labels = labels.to(device)
        outputs = model(images)
        # max returns (value ,index)
        values, predicted = torch.max(outputs.data, 1)
        n_samples += labels.size(0)
        n_correct += (predicted == labels).sum().item()

        class_probs_batch = [F.softmax(output, dim=0) for output in outputs]

        class_preds.append(class_probs_batch)
        class_labels.append(labels)

    # 10000, 10, and 10000, 1
    # stack concatenates tensors along a new dimension
    # cat concatenates tensors in the given dimension
    class_preds = torch.cat([torch.stack(batch) for batch in class_preds])
    class_labels = torch.cat(class_labels)

    acc = 100.0 * n_correct / n_samples
    print(f'Accuracy of the network on the 10000 test images: {acc} %')

    ############## TENSORBOARD ########################
    classes = range(10)
    for i in classes:
        labels_i = class_labels == i
        preds_i = class_preds[:, i]
        writer.add_pr_curve(str(i), labels_i, preds_i, global_step=0)
        writer.close()
    ###################################################

运行
http://localhost:6006
,可以得到下图,可以根据图中的曲线等信息进行分析学习结果。

image
image


传送门:
零基础学习人工智能—Python—Pytorch学习—全集


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18601612


ServiceMesh系列

1 背景

在复杂的互联网场景中,不可避免的会出现请求失败或者超时的情况。
从程序的的响应结果来看,一般是Response返回5xx状态的错误;从用户的角度去看,一般是请求结果不符合预期,即操作失败(如转账失败、下单失败、信息获取不到等)。
偶发的不可避免的5xx请求错误,产生的原因有很多种,比如:

  • 网络延迟或者抖动
  • 服务器资源不足(CPU、内存走高、连接池满)
  • 服务器故障
  • 符合某些特定条件下的服务程序bug(大都非必现)
    image

2 系统稳定性等级划分

大部分服务容忍低频、偶发的5xx错误,并使用可用性级别来衡量系统的健壮性,级别系数越高,健壮性越好,如下:

等级描述 故障时长(年) 可用行等级
基本可用性 87.6h 99%
较高可用 8.8h 99.9%
非常高的可用性(大部分故障可自动恢复) 52m 99.99%
极高可用性 5m 99.999%

对于强系统可靠性、强结果预期性 要求的系统,如转账、下单、付款,即使微小的可用性降级也是不可接受,用户强烈需要接收到正确的结果。
可以想想你付款的时候发现付款失败有多么惊慌,订外卖的时候获取信息失败有多么沮丧,这些都是用户痛点。

3 请求异常的治理手段

3.1 采用异常重试实现故障恢复

通过上面的故障原因分析我们知道,排除了必现的程序逻辑错误,大部分环境导致的错误是可以通过重试进行恢复的。
治理的手段主要是采用 异常重试 来实现的,通过重试负载到健康实例上(实例越多重试成功率越高),降低用户感知到的故障频率。
image

执行过程说明

  • 这边以示例服务 Svc-A 向 Svc-B 发起访问为例子。
  • 第1次执行失败之后,根据策略,间隔25ms之后发起第2次请求。
  • 会看到有两条日志,日志的trace_id 一致,说明他是同一个调用过程(1个调用过程,包含2次请求,首发1次与重试1次)
  • 请求方为同一个实例 Svc-A-Instance1,说明请求发起方一致。
  • 被请求方发生了变动,说明调度到新的实例(Svc-B-Instance1 到 Svc-B-Instance2)。
  • 返回正常的 200 。

因为我们的负载均衡模式默认是RR,所以实例越多,实际上重试成功的概率会越高。比如有50个实例,其中一个实例出故障,导致执行返回5xx,那么第二次请求的时候一般来说会有 49/50 的成功概率。如下图:
image

3.2 Istio策略实现

注释比较清晰了,这边就不解释了。

# VirtualService
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: xx-svc-b-vs
  namespace: kube-ns-xx
spec:
  hosts:
  - svc_b.google.com # 治理发往 svc-b 服务的流量
  http:
  - match:  # 匹配条件的流量进行治理
    - uri:
        prefix: /v1.0/userinfo   # 匹配路由前缀为 /v1.0/userinfo 的,比如 /v1.0/userinfo/1305015
    retries:
      attempts: 1  # 重试一次
      perTryTimeout: 1s  # 首次调用和每次重试的超时时间
      retryOn: 5xx  # 重试触发的条件
    timeout: 2.5s  #  请求整体超时时间为2.5s,无论重试多少次,超过该时间就断开。
    route:
    - destination:
        host: svc_b.google.com
      weight: 100
  - route:  # 其他未匹配的流量默认不治理,直接流转
    - destination:
        host: svc_c.google.com
      weight: 100

4 请求超时的治理手段

4.1 请求超时的主要原因

  • 网络延迟或者抖动或者丢包,从而导致响应时间变长。
  • 容器甚至云主机资源瓶颈情况:如CPU使用率过高、内存使用是否正常、磁盘IO压力情况、网络时延情况等资源使用情况异常,也可能导致响应时间变长。
  • 负载均衡性问题:多实例下分配的流量不均衡,目前看云基础场景,这个情况不多见。
  • 突发洪峰请求:如流不存在非预期的流量,作为主打对内的项目,突发洪峰请求主要还是程序的调用不合理或者程序bug(内存泄露、循环调用、缓存击穿等)。
    image
    单个副本,长耗时容易造成队列堆积,对资源损耗很大,快速的释放或者调度开是一个比较好的办法,是一种普遍可接受的降级方案,否则超时阻塞会导致服务长时间不可用。
    而且这种影响是水平扩散的,同服务上的其他功能也会被争抢资源。

4.2 Istio的治理手段

4.2.1 超时重试

对服务的核心接口进行细粒度配置,具体接口超时时间应该在 ≥ TP 99.9(满足999‰的网络请求所需要的最低耗时)的耗时,可以考虑重试。
image

4.2.2 超时熔断

通过指定超时时间对请求进行断连,达到降级的目的。避免长时间队列阻塞,导致雪崩沿调用向上传递,造成整个链路崩溃。
image

4.3 Istio策略实现

关注下方代码中的两个星号 ★ 的属性:

  • perTryTimeout 指的是首次调用和每次重试的超时时间,超过这个时间,说明请求大概率已经pending住了,则进行重试,争取落到其他健康实例上,更快拿回的结果。
  • timeout 指的是请求整体超时时间为2.5s,无论重试多少次,超过该时间就断开,这是一种保护策略,避免过度重试或者长时间Pending导致服务恶化甚至雪崩。
# VirtualService
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: xx-svc-b-vs
  namespace: kube-ns-xx
spec:
  hosts:
  - svc_b.google.com # 治理发往 svc-b 服务的流量
  http:
  - match:  # 匹配条件的流量进行治理
    - uri:
        prefix: /v1.0/userinfo   # 匹配路由前缀为 /v1.0/userinfo 的,比如 /v1.0/userinfo/1305015
    retries:
      attempts: 1  # 重试一次
      perTryTimeout: 1s  #  ★ 首次调用和每次重试的超时时间
      retryOn: 5xx  # 重试触发的条件
    timeout: 2.5s  #  ★ 请求整体超时时间为2.5s,无论重试多少次,超过该时间就断开。
    route:
    - destination:
        host: svc_b.google.com
      weight: 100
  - route:  # 其他未匹配的流量默认不治理,直接流转
    - destination:
        host: svc_c.google.com
      weight: 100

5 总结

本文我们介绍了使用服务网格进行异常重试和超时熔断的治理。Istio提供了丰富的治理能力,后续的章节我们逐一了解下故障注入、熔断限流、异常驱逐等高级用法。

基础概念

这是
人工评估
系列文章的第一篇《基础概念》,全系列包括:

  • 基础概念
  • 人工标注员
  • 技巧与提示

什么是人工评估?

人工评估是指让人类评价模型输出回答的好坏。
本文讨论的都是后验评估,即模型已经完成训练,给定一个任务让人类进行评估。

系统化评估

系统化的人工评估主要有 3 种方式:

如果你手头
没有现成的数据集
,但还是想测试一些模型的能力,可以采用人工评估:提供一个任务说明和打分指南 (例如:
尝试与模型交互,迫使模型输出不当语言,即包含冒犯性、歧视性、暴力等。如果模型输出了不当语言,则得分为 0,反之为 1。
),以及可供交互的测试模型,然后就可以让标注员人工操作并评分,同时列出评分理由。

如果你手头
已经有数据集
(例如
收集了一组 prompt,并确保这些 prompt 不会迫使模型输出不当回答
),可以自行将 prompt 输入模型得到输出,然后将输入 prompt、输出回答、打分指南一起提供给标注员评估 (
如果模型意外输出不当,则得分为 0,反之为 1
)。

如果你手头
既有数据集也有评分结果
,可以让人工标注员通过
错误注释
的方法 (
这种方法同样可以作为评估系统,适用于上面的情况
) 来对评估进行审查。在测试新评估系统时,这一步非常重要,但是技术测层面属于对评估系统的评估,因此略微超出本文的讨论范围。

注:

  • 如要对已部署的生产模型做评估,可以考虑进行人工 A/B测试及反馈。
  • AI 审计 (AI audits)
    (模型外部系统评估) 也是一种人工评估方式,但不在本文讨论范围。

非正式评估

基于人类的评估方法还有两种不那么正式的方法:

Vibes 检查
是一种使用非公开数据进行人工评估的方法,用来在多个场景用例 (如代码编程和文学创作等) 上测试来把握整体效果。评估结果通常会被当作轶事证据而分享在 Twitter 和 Reddit 上,不过它们很容易受到主观认知偏差的影响 (换句话说,人们往往只相信自己相信的结果)。尽管如此,这些结果依然能作为
你自己测试的一个不错起点

Arenas
是一种众包人工评估的方法,用来给多个模型表现排名。
一个知名的例子是
LMSYS 聊天机器人 Arena 评估
, 社区用户通过与多个模型对话来分辨孰优孰劣并投票。总的投票结果将汇总为 Elo 排名 (这场多个模型比赛的排名),来评判出 “最优模型”。

人工评估的优劣势

优势:

  • 灵活性
    :只要评估定义的足够明确,人工评估几乎适用于所有任务!
  • 无数据污染
    :人工书写的问题 prompt 不会跟训练集有交叉 (希望如此)。
  • 与人类偏好的相关性
    :这条显而易见,毕竟是按人工标准来评分的。
    注:进行人工评估时,尽量确保标注员的多样性,以保证评估结果的泛化性。

劣势:

  • 第一印象偏差
    :人工标注员往往根据
    第一印象
    来评估回答的质量,有时候会忽略对事实的考证。
  • 语气偏差
    :众包标注员对语气特别敏感,容易低估一些表述比较坚定的句子而出现事实或逻辑错误。比如模型以自信的语气说出错误的内容,标注员可能很难发觉,进而导致对输出更为自信的模型的评分偏高。相比之下,专家标注员受语气偏差的影响更低。
  • 自我偏好偏差
    :人们有时候会
    偏向于选择迎合自己观点的答案
    ,而不是事实正确的答案。
  • 身份背景偏差
    :不同身份背景的人具有不同的价值观,可能导致评估模型时表现出显出差异 (例如在模型输出的
    不当回答评估
    中,对何为不当表述的理解偏差)。

系统化人工评估

系统化人工评估 (尤其是付费的人工) 的优势:

  • 高质量数据
    :可以根据评估任务量身定制测试集,为你开发 (例如需要开发偏好模型) 和评估模型提供进一步支持。
  • 数据隐私
    :付费标注员 (尤其是内部人员) 通常很注重数据安全性,反而 LLM 评估的闭源 API 模型的数据隐私性较差,因为你需要将个人数据发送给外部服务。
  • 可解释性
    :标注员在评分时会清晰的说明打分理由。

缺点:

  • 成本较高
    :当然你需要支付给标注员费用。甚至为了优化评估指南,你还需要多轮迭代,这会使得费用更高。
  • 扩展性差
    : :除非你的评估任务非常依赖用户反馈,否则人工评估方法的扩展性确实不太好,因为每次进行一轮新的评估都需要重新调动人员 (并支付费用)。
  • 重复性低
    :除非你能保证每次评估都是同一批标注员并且评分标准完全明确,否则不同的标注员对评估结果的可能无法精确复现。

非正式人工评估

优势:

  • 成本较低
    :社区成员自愿参与,费用支付较少。
  • 发现边缘用例
    :由于限制较少,成员自发的创造性可能会发现一些有趣的边缘用例。
  • 扩展性高
    :只要有足够多的社区成员自愿参与,评估的扩展性就会更好,且参与门槛较低。

劣势:

  • 高度主观性
    :由于社区成员的自身的
    文化局限性
    ,尽管标准一致,也很难在评分时保持一致性。不过 “群体智慧” 效应 (参考 Galton 的 Wiki 页面) 可能在大量的评分投票中平滑地缓解这一问题。
  • 评分偏好不具代表性
    :由于年轻西方男性在互联网技术社区中的占比过高,可能导致评估的偏好严重失衡,这跟实际上普通大众的口味并不一致,因此会影响评估的准确性。
  • 容易被操控
    :如果你请的众包标注员没经过筛选,第三方机构很容易通过操控他们来导致模型的评分异常 (如偏高),尤其是当模型的写作风格比较独特的时候。

原文链接:
https://github.com/huggingface/evaluation-guidebook/blob/main/contents/human-evaluation/basics.md

译者: SuSung-boy, clefourrier, adeenayakup