wenmo8 发布的文章

参考:
https://zhuanlan.zhihu.com/p/610049537?utm_id=0

有一些桌面用户想以 root 身份登录。这不是什么明智之举,但肯定是可以做到的。

默认情况下,Ubuntu 禁用了 root 账户。你必须使用
sudo
命令来执行任何需要 root 权限的任务。

当然,这是为了你自己的安全。一直以 root 身份使用系统,就像手里拿着一把剑到处跑。它增加了把事情搞乱的机会。

以 root 身份登录在服务器中仍然很常见。在桌面方面,以 root 身份登录的情况相当少见。甚至 Kali Linux 也做了改变。

然而,有一些桌面用户想以 root 身份登录。这不是什么明智之举,但肯定是可以做到的。

在本指南中,我将向你展示如何在 Ubuntu 中
以 root 身份登录 GNOME 桌面

如何在 GNOME 桌面上以 root 身份登录

我不建议在桌面上以 root 身份登录。你有 sudo 机制来满足你所有的 root 需求。只有在你有足够理由的情况下才这样做。本教程仅用于演示目的。你已经被警告过了。

步骤 1:启用 root 账户

你想以 root 身份登录。但默认情况下,root 账户是禁用的。第一步是启用它。

改变 root 账户的密码,这将为你启用 root 账户:

sudo passwd root

image

不言而喻,你不应该忘记 root 密码。

步骤 2:改变 GDM 配置

本教程的这一部分只对 GNOME 有效。请
检查你的桌面环境
并确保它是 GNOME。

Ubuntu 默认使用 GNOME,GNOME 使用 GDM
显示管理器

为了允许以 root 身份登录到 GNOME,你需要对位于
/etc/gdm3/custom.conf
的 GDM
配置文件
做一些修改。

对该配置文件做一个备份:

cp /etc/gdm3/custom.conf /etc/gdm3/custom.conf~

在最坏的情况下,如果你以某种方式把事情搞砸了,备份的文件可以用来从 TTY 上替换现有文件。

现在,用以下命令打开 GDM 文件:

sudo nano /etc/gdm3/custom.conf

并添加以下一行,允许 root 用户登录:

AllowRoot=true

image


Ctrl+X
退出 Nano,同时保存它。

步骤 3:配置 PAM 认证

现在,你必须配置 PAM 认证
守护进程
文件,它可以通过以下命令打开:

sudo nano /etc/pam.d/gdm-password

在这个文件中,你必须注释掉以下带有
#
号的一行,该符号拒绝 GUI 中的 root 访问:

auth   required        pam_succeed_if.so user != root quiet_success

image

保存修改并退出 nano文本编辑器

步骤 4:以 root 身份登录

现在,重启你的系统:

reboot

在登录界面,选择
Not listed
选项,在用户名中输入
root
,并输入你在本指南开头配置的密码:

image

当你登录后,它就会通知你,
“logged in as a privileged user”

image

这就完成了! 现在,你正在以 root 身份运行你的 Ubuntu 系统。

接口测试在软件开发生命周期中扮演着至关重要的角色,有助于验证不同模块之间的交互是否正确。若协议消息被恶意修改,系统是否能够恰当处理,以确保系统的功能正常运行,不会出现宕机或者安全问题。

5.1 ChatGPT在接口测试中的角色

接口测试是确保系统各个模块协同工作的关键环节。传统的接口测试用例编写通常依赖于测试人员的经验、手动操作,这可能导致接口测试用例的不充分和测试内容的遗漏。ChatGPT能够理解和生成文本使其成为编写接口测试用例的理想工具。

5.1.1 理解系统需求和接口规范

ChatGPT可以用来解析系统需求规格说明书和接口文档。通过输入相关文档,ChatGPT可以生成摘要、要点和问题列表等,帮助测试人员更好地把握测试的范围和重点。

假设我们正在测试一个电子商务网站的购物车接口。我们有系统需求文档和接口文档,但它们很冗长,需要花费大量的时间阅读、理解。但是有了ChatGPT以后,只需要将这些文档输入ChatGPT,它就可以快速给出摘要,列出所有与购物车相关的功能和要点等。这有助于快速地确定测试重点,确保接口测试用例的全面性。

5.1.2 生成测试用例模板

ChatGPT可以协助测试团队生成测试用例的模板,包括输入数据、操作步骤和预期结果等。测试人员可以向ChatGPT提供接口描述和测试目标,ChatGPT会自动生成基本的测试用例结构。测试用例模板化以后,既可以加快测试用例的编写速度,又能确保测试用例的一致性。

对于一个社交媒体平台的接口测试,我们需要测试用户发布帖子的功能。通过描述接口规范和测试目标,ChatGPT可以自动生成测试用例模板,包括帖子内容、发布操作的步骤和预期的发布结果等。这样,测试人员只需填写具体的内容,而不必从头开始编写每个接口测试用例。

例如,对于一个电子商务网站的购物车接口,ChatGPT生成的接口文档如下所示。

ChatGPT
生成的接口文档

接口名称:
add_to_cart

接口概述:
该接口用于将指定商品添加到购物车。

请求方式:
POST请求

请求
URL

https://example.com/api/add_to_cart

参数列表:

  • item_id(商品ID)

参数描述:用于指定要添加到购物车的商品的唯一标识。

参数类型:整数

是否必填:是

  • num(数量)

参数描述:指定要添加的商品数量。

参数类型:整数

是否必填:是

请求示例:

POST请求 https://example.com/api/add_to_cart

请求体:

{

"item_id": 12345,

"num": 2

}

响应示例:

{

"status": 200,

"message": "添加商品到购物车成功",

"data": {

"cart_total_items": 5

}

}

响应字段说明:

status:标识业务是否成功的状态码。

  • 200 表示请求成功,商品成功添加到购物车。
  • 其他状态码可根据具体情况自行定义。

message:状态信息,对状态码的详细描述。

data:包含响应数据。

  • cart_total_items:购物车中的总商品数量。

5.1.3 探索边界条件

在接口测试中,通常需要测试各种输入数据的边界条件和异常情况。ChatGPT可以帮助测试人员生成边界条件的接口测试用例,确保系统能够正确处理各种情况。

在电子商务网站的购物车接口的测试中,我们需要确保它能够正确处理购买的商品数量的输入,包括正常的商品数量输入和异常地输入。异常输入可能包括带小数点的数字、字母、全角数字、中文字符以及货币符号等特殊字符。不同类型的数据库具有不同的数字上下限,例如,在MySQL中,SMALLINT类型占用2个字节,可以存储从-32768到32767的整数。因此,我们需要考虑商品数量超过上限、低于下限、等于上限、等于下限以及0作为特殊数字和正常数字等各种边界情况的测试用例。

很多情况下,通过UI无法提交的数字,若接口测试通过协议直接发送请求,在应用程序的后台没有进行校验并且数据库没有添加约束条件的情况下,仍然可以正常提交,这可能导致数据无法正常存储等严重问题。

例如,购物车中单个商品数量最大可以为9999,考虑边界值测试用例方法设计接口测试用例,则可以获得以下边界用例,ChatGPT生成的边界值接口测试用例如下所示。

ChatGPT
生成的边界值接口测试用例

用例编号:TC001

用例名称:添加数量为0的商品

输入参数:

item_id: 123456

num: 0

预期结果:

添加失败,提示数量不能为0

......

用例编号:TC005

用例名称:添加超过库存的商品

输入参数:

item_id: 123456

num: 10000

预期结果:

添加失败,提示超过库存

......

本文分享自华为云社区
《GaussDB(DWS)查询过滤器过滤规则原理与使用介绍》
,作者: 清道夫。

1. 前言

适用版本:【9.1.0.100(及以上)】

查询过滤器在
9.1.0.100
之前就具备提供查询过滤功能的能力,但仅支持自动隔离反复查询被终止的查询,防止烂SQL再次执行。

老版本主要面向异常熔断机制和紧急拦截场景,前者可以与异常规则联动,自动将触发异常规则的语句添加到黑名单中,后者是需要手动找到core或者引发hang的语句进行屏蔽。

大家有兴趣可以翻一下之前的这篇文章
GaussDB(DWS)查询过滤器原理与应用

9.1.0.100

9.1.0.200
版本对查询过滤器做了功能的改进,可以通过多维度进行烂SQL识别,功能更丰富,配置更灵活。

2. 原理介绍

在原理介绍之前,先举个简单的例子。在业务开发过程中,要想禁止对2张以上的表进行关联查询,此时可以使用DDL语句创建过滤规则:

CREATE BLOCK RULE forbid_2_t_sel FOR SELECT FILTER BY  SQL('test_block_rule') with(table_num='2');

table_num
指的是一个语句中出现的表的个数,此时所有查询语句不能包含有两张表以上的查询。

--两张表直接关联查询,可以正常执行
postgres=# select * from test_block_rule1 t1 join test_block_rule2 t2 on t1.c1=t2.c2;
 c1 | c2 | c1 | c2
----+----+----+----
(0 rows)

--三张表直接关联查询,被拦截
postgres=# select * from test_block_rule1 t1 join test_block_rule2 t2 on t1.c1=t2.c2 join test_block_rule3 t3 on t2.c1=t3.c1;
ERROR:  hit block rule forbid_2_t_sel(block_type: SELECT, regexp_sql: test_block_rule, table_num: 2(3))

说到这,整体逻辑就非常清楚了。用户可以提前识别烂SQL的特征,然后抽象出来,用DDL语句创建规则,后续会对查询的语句进行过滤,被规则筛选出来的便是烂SQL,执行前会报错,反之则可以正常执行。

查询过滤器框架及功能原理概况:

从图中可以看出,之前的查询过滤器的功能依然存在,可以保证与异常规则的联动,新版本的增强更注重规则的灵活性和功能的丰富性。

3. 使用介绍

3.1 查询过滤规则元数据管理

查询过滤规则,可以通过DDL进行新增、删除或者修改,其语法如下:

(1)创建

CREATE BLOCK RULE [ IF NOT EXISTS ] block_name
    [ [ TO user_name@'host' ] | [ TO user_name ] | [ TO 'host' ] ] |
    [ FOR UPDATE | SELECT | INSERT | DELETE | MERGE ] |
    FILTER BY
    { SQL ( 'text' ) | TEMPLATE ( template_parameter = value ) }
    [ WITH ( { with_parameter = value }, [, ... ] ) ];

其中,

  • block_name:
    过滤规则的名称

  • user_name:
    规则应用的对象用户

  • host:
    是规则应用的客户端IP

  • FOR:
    语句类型,支持对UPDATE/SELECT/INSERT/DELETE/MEGE INTO五种类型语句进行限制

  • FILTER BY:
    过滤方法,包含两种形式

  • SQL:
    根据关键词对语句进行正则匹配,例如表名,其长度不能超过1024,建议尽量精简

  • TEMPLATE:

  • unique_sql_id:
    归一化的64位哈希值,重复概率较sql_hash大一些

  • sql_hash:
    归一化的哈希值(md5),一般不会重复,相较unique_sql_id更推荐使用

  • with_parameter:
    查询过滤规则选项参数,可以附加多个条件,满足其一便会匹配过滤。

  • application_name:
    客户端名称

  • query_band:
    负载标识

  • table_num:
    包含的基表个数

  • partition_num:
    扫描分区的数量

  • estimate_row:
    基表输出行数

  • resource_pool:
    切换的目标资源池,仅适用于
    9.1.0.200
    及以上

  • max_active_num:
    可并发执行的语句个数,仅适用于
    9.1.0.200
    及以上

  • is_warning:
    改变拦截行为为告警,而非默认的报错,仅适用于
    9.1.0.200
    及以上

其中,
user_name

FILTER BY
是必选项,其他可以通过业务实际需要进行配置。

(2)修改

ALTER BLOCK RULE block_name RENAME to new_block_name;

通过
rename
对查询过滤的规则进行重命名。

ALTER BLOCK RULE block_name
    [ [ TO user_name@'host' ] | [ TO user_name ] | [ TO 'host' ] | [ TO DEFAULT ] ] |
    [ FOR UPDATE | SELECT | INSERT | DELETE | MERGE | DEFAULT ] |
    [ [ FILTER BY ]
    [ { SQL ( 'text' ) | TEMPLATE ( template_parameter = value ) } ] ]
    [ WITH ( { with_parameter = value }, [, ... ] ) ];

所有选项均支持二次修改,如果需要去除部分字段的限制,可以指定
default
关键词,例如:

--修改为只能查询1张表
postgres=# ALTER BLOCK RULE forbid_2_t_sel with(table_num='1');
ALTER BLOCK RULE
postgres=# select * from test_block_rule1 t1 join test_block_rule2 t2 on t1.c1=t2.c2;
ERROR:  hit block rule forbid_2_t_sel(block_type: SELECT, regexp_sql: test_block_rule, table_num: 1(2))
postgres=# select * from test_block_rule1 t1;
 c1 | c2
----+----
(0 rows)

--去除查询中表个数的限制
postgres=# ALTER BLOCK RULE forbid_2_t_sel with(table_num=default);
ALTER BLOCK RULE
--再次查询报错拦截
postgres=# select * from test_block_rule1 t1;
ERROR:  hit block rule forbid_2_t_sel(block_type: SELECT, regexp_sql: test_block_rule)

(3)删除

DROP BLOCK RULE [ IF EXISTS ] block_name;

3.2 权限问题

对于普通用户来讲是没有创建查询过滤规则权限的,需要管理员或者管理员将权限赋给某一普通用户才可以。

--切换至普通用户
postgres=# set role jack password 'xxx';
SET
--创建查询过滤规则报错提示无权限
postgres=> create block rule bl2 filter by sql('test');
ERROR:  CREATE/ALTER/DROP BLOCK RULE permission denied for current user
--重置user
postgres=> reset role;
RESET
--对普通用户进行授权
postgres=# grant gs_role_block to jack;
GRANT ROLE
--切换普通用户
postgres=# set role jack password 'xxx';
SET
--再次创建成功
postgres=> create block rule bl2 filter by sql('test');
CREATE BLOCK RULE

建议创建查询过滤规则时尽量缩小适用范围,避免误过滤,或者范围过大导致性能劣化。

3.3 备份恢复

对于查询过滤规则的备份或者恢复的权限与操作元数据的权限一致,需要管理员或者管理员讲权限赋值给某一普通用户才可以,用户可以通过
gs_dump
导出查询过滤规则定义。

如果想查看或者导入查询过滤规则的定义,可以通过
pg_get_blockruledef
进行查询。

postgres=# select * from pg_get_blockruledef('test');
                         pg_get_blockruledef
----------------------------------------------------------------------
 CREATE BLOCK RULE test FILTER BY SQL('test') WITH(estimate_row='3');
(1 row)

所有的查询过滤规则元数据全部保存在
pg_blocklists
系统表中,可以通过查看系统表浏览所有的查询过滤规则。

所有的查询过滤规则元数据全部保存在
pg_blocklists
系统表中,可以通过查看系统表浏览所有的查询过滤规则。

3.4 使用举例

(1)使用关键词进行查询过滤

CREATE BLOCK RULE bl1
To block_user
FOR SELECT
FILTER BY SQL ('tt')
WITH(partition_num='2',
     table_num='1',
     estimate_row='5'
     );

postgres=> select * from tt;
ERROR:  hit block rule bl1(user_name: block_user, block_type: SELECT, regexp_sql: tt, partition_num: 2(3), table_num: 1(1), estimate_row: 5(1))

从上面的查询可以看出,查询语句包含了
tt
关键字,并且扫描的分区个数超过了
2
,此时执行语句被过滤拦截。需要注意的是,
扫描分区的个数并不总是准确的
,仅能识别
静态
的分区剪枝个数,执行过程中的
动态剪枝
并不能被识别。

小技巧:
使用关键词过滤时可以先使用正则匹配符
~*
进行测试,正则匹配是忽略大小写的。

另外,由于查询过滤器的规则直接作用在用户
block_user
上,因此在删除用户
block_user
时,会提示有依赖项,此时可以通过在语句最后加上
cascade
进行删除,此时作用在此用户上的查询过滤规则也会被一同删除。

受限于篇幅,其他选项就不再一一列举。需要注意的是,过滤规则命中的依据是,
with_parameter
命中任意一项,且其他字段的特征也符合即会判定为符合查询过滤规则。

特别注意,不同的计划,可能部分字段无法按照预期进行拦截,例如:

postgres=# create block rule test filter by sql('test')with(estimate_row='3');
CREATE BLOCK RULE
postgres=# select * from test;
 c1 | c2
----+----
  1 |  2
  1 |  2
  1 |  2
  1 |  2
  1 |  2
(5 rows)

此时,语句关键字是可以匹配上的,查询的行数也超过了3行的限制,那为什么无法拦截呢?

postgres=# explain verbose select * from test;
                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
  id |                  operation                   | E-rows | E-distinct | E-width | E-costs
 ----+----------------------------------------------+--------+------------+---------+---------
   1 | ->  Data Node Scan on "__REMOTE_FQS_QUERY__" |      0 |            |       0 | 0.00

      Targetlist Information (identified by plan id)
 --------------------------------------------------------
   1 --Data Node Scan on "__REMOTE_FQS_QUERY__"
         Output: test.c1, test.c2
         Node/s: All datanodes (node_group, bucket:16384)
         Remote query: SELECT c1, c2 FROM public.test

通过计划可以看出,此时是
FQS
计划,导致没有估算信息。因此此时无法进行拦截,对于CN轻量化的计划也是一样的,如果我们让语句强制走
stream计划
,那么就可以拦截成功:

postgres=# set enable_stream_operator=on;
SET
postgres=# set enable_fast_query_shipping=off;
SET
postgres=# select * from test;
ERROR:  hit block rule test(regexp_sql: test, estimate_row: 3(5))
postgres=#  explain verbose select * from test;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
  id |               operation                | E-rows | E-distinct | E-width | E-costs
 ----+----------------------------------------+--------+------------+---------+---------
   1 | ->  Row Adapter                        |      5 |            |       8 | 69.00
   2 |    ->  Vector Streaming (type: GATHER) |      5 |            |       8 | 69.00
   3 |       ->  CStore Scan on public.test   |      5 |            |       8 | 59.01

      Targetlist Information (identified by plan id)
 --------------------------------------------------------
   1 --Row Adapter
         Output: c1, c2
   2 --Vector Streaming (type: GATHER)
         Output: c1, c2
         Node/s: All datanodes (node_group, bucket:16384)
   3 --CStore Scan on public.test
         Output: c1, c2
         Distribute Key: c1

所以,如果估算信息不准确,也会导致误拦截或者漏拦截的情况,因为计划的信息是通过估算得到的,因此这种情况无法避免。

(2)使用语句归一化特征值进行查询过滤

语句归一化的特征值,目前有两个,分别是
unique_sql_id

sql_hash
,两者均是对查询树进行哈希计算之后得出的,区别在于前者是64位哈希值,后者是md5值,因此前者的重复概率会大于后者,在使用时尽量使用
sql_hash
进行过滤。

很多小伙伴会问,这两个值如何获取呢?两种方法:

  • 查看explain结果

postgres=> explain verbose select * from tt where a>1;
                                              QUERY PLAN
 ----------------------------------------------------------------------------------------------------
   id |                     operation                     | E-rows | E-distinct | E-width | E-costs
  ----+---------------------------------------------------+--------+------------+---------+---------
    1 | ->  Row Adapter                                   |      1 |            |       8 | 16.00
    2 |    ->  Vector Streaming (type: GATHER)            |      1 |            |       8 | 16.00
    3 |       ->  Vector Partition Iterator               |      1 |            |       8 | 6.00
    4 |          ->  Partitioned CStore Scan on public.tt |      1 |            |       8 | 6.00

    Predicate Information (identified by plan id)
  -------------------------------------------------
    3 --Vector Partition Iterator
          Iterations: 3
    4 --Partitioned CStore Scan on public.tt
          Filter: (tt.a > 1)
          Pushdown Predicate Filter: (tt.a > 1)
          Partitions Selected by Static Prune: 1..3

  Targetlist Information (identified by plan id)
  ----------------------------------------------
    1 --Row Adapter
          Output: a, b
    2 --Vector Streaming (type: GATHER)
          Output: a, b
          Node/s: datanode1
    3 --Vector Partition Iterator
          Output: a, b
    4 --Partitioned CStore Scan on public.tt
          Output: a, b

               ====== Query Summary =====
  -----------------------------------------------------
  Parser runtime: 0.029 ms
  Planner runtime: 0.286 ms
  Unique SQL Id: 2229243778
  Unique SQL Hash: sql_aae71adfaa3d91bfe75499d92ad969e8
 (34 rows)
  • 查看topsql记录

 queryid                     | 95701492082350773
 query                       | select * from tt where a>10;
 query_plan                  | 1 | Row Adapter  (cost=14.00..14.00 rows=1 width=8)
                             | 2 |  ->Vector Streaming (type: GATHER)  (cost=0.06..14.00 rows=1 width=8)
                             | 3 |   ->Vector Partition Iterator  (cost=0.00..4.00 rows=1 width=8)
                             |   |     Iterations: 2
                             | 4 |    ->Partitioned CStore Scan on public.tt  (cost=0.00..4.00 rows=1 width=8)
                             |   |      Filter: (tt.a > 10)
                             |   |      Pushdown Predicate Filter: (tt.a > 10)
                             |   |      Partitions Selected by Static Prune: 2..3
 node_group                  | installation
 pid                         | 139803379566936
 lane                        | fast
 unique_sql_id               | 2229243778
 session_id                  | 1732413324.139803379566936.coordinator1
 min_read_bytes              | 0
 max_read_bytes              | 0
 average_read_bytes          | 0
 min_write_bytes             | 0
 max_write_bytes             | 0
 average_write_bytes         | 0
 recv_pkg                    | 2
 send_pkg                    | 2
 recv_bytes                  | 3297
 send_bytes                  | 57
 stmt_type                   | SELECT
 except_info                 |
 unique_plan_id              | 0
 sql_hash                    | sql_aae71adfaa3d91bfe75499d92ad969e8

可以看出两种方法都可以轻松获取这两个语句归一化的特征值,
explain
可以在事前提前获取,
topsql
可以在语句执行后进行获取。

这个时候,可能很多小伙伴又会有疑问,语句中的条件有变化,是否会影响归一化的特征值呢?

答案是不会,因为归一化过程中会去除常量的影响,上述的举例中两个语句条件中的常量值并不相同,但归一化的特征值确实一样的。

(3)查询过滤的性能

由于语句的过滤,特别是关键词的正则匹配通常是比较耗时的,此时如果有过多的过滤规则,可能导致执行时间的劣化,特别是对于短查询可能影响更为明显。

本地实测:
正则匹配关键词长度1024,建立查询过滤规则1000条左右时,对于查询的影响在27.72ms左右,且如果考虑其他匹配项,可能影响会更大,所以,不建议添加太多的查询过滤规则。且业务稳定后可以只对特定开发或者新业务的用户创建查询过滤规则,此时查询过滤规则会优先通过绑定的用户跳过无效的过滤,减少对性能的性能的影响。

(4)过滤时间查看

可以配置GUC参数
analysis_options
查看查询过滤规则对正常语句所消耗的时间。

set analysis_options='on(BLOCK_RULE)';

-- explain performance + query

                    User Define Profiling
-----------------------------------------------------------------
Segment Id: 3  Track name: Datanode build connection
      datanode1 (time=0.288 total_calls=1 loops=1)
      datanode2 (time=0.301 total_calls=1 loops=1)
      datanode3 (time=0.321 total_calls=1 loops=1)
      datanode4 (time=0.268 total_calls=1 loops=1)
Segment Id: 3  Track name: Datanode wait connection
      datanode1 (time=0.016 total_calls=1 loops=1)
      datanode2 (time=0.038 total_calls=1 loops=1)
      datanode3 (time=0.021 total_calls=1 loops=1)
      datanode4 (time=0.017 total_calls=1 loops=1)
Segment Id: 1  Track name: block rule check time
      coordinator1 (time=0.028 total_calls=1 loops=1)

(5)拦截记录

[仅适用于
9.1.0.200
及以上]

创建查询过滤规则后会拦截很多烂SQL,如何看拦截的语句有哪些呢?可以通过topsql进行查看,
abort_info
会记录拦截信息,也就是查询的报错信息。

postgres=# select abort_info,query from GS_WLM_SESSION_INFO where abort_info like '%hit block rule test%';
                        abort_info                         |        query
-----------------------------------------------------------+---------------------
 hit block rule test(regexp_sql: test, estimate_row: 3(5)) | select * from test;
(1 rows)

4. 总结

查询过滤器在
9.1.0.100

9.1.0.200
版本丰富了大量的功能,提高了烂SQL拦截的灵活性。

管控面后续版本同样可以直接通过前端页面对查询过滤规则进行管理,大家敬请期待。

有任何问题欢迎留言讨论,我们将不断丰富和完善查询过滤功能,让烂SQL无门可入。

华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。
点击链接
,免费领取您的专属云主机

点击关注,第一时间了解华为云新鲜技术~

为什么需要主备结构?

为了确保服务的高可用性,系统不能因为某一个节点的故障而完全不可用。因此,我们需要通过主备结构来确保在主节点发生故障时,备份节点能够迅速接管,继续提供服务。

为什么不直接通过多个节点共同提供服务?

因为与应用服务不同,这种节点提供的是数据维护和存储服务,为了确保数据的连续性和一致性,就只能由一个节点集中处理。如果多个节点同时处理数据,可能会导致数据的不一致或混乱。

比如:Mysql主从复制,Kafka的分区副本,Redis的主从复制。

主备结构是如何运作的?

主备结构通常由两个角色组成:
Leader
(主节点)和
Follower
(备份节点)。在这个结构中,Leader节点负责处理所有的客户端请求,维护数据。而Follower节点则定期从Leader节点同步数据。当Leader节点发生故障或不可用时,系统就需要进行
选主
操作。选主的目的是从现有的Follower节点中选出一个新的Leader,以确保系统的正常运行。

选主的核心工作是什么?

选主的核心任务可以总结为以下几点:

  1. 监控Leader节点的状态
    :持续检测Leader节点的健康状况。
  2. 选举新的Leader
    :当Leader节点挂掉时,从Follower节点中选举出一个新的Leader。
  3. 通知其他Follower节点
    :新Leader选出后,需要通知其他Follower节点,让它们切换到新Leader同步数据。

谁来负责选主?

选主操作通常由两种方式来处理:

  1. 第三方协调者
    :通过引入一个独立的第三方系统来协调选主,比如Zookeeper、etcd等。这些工具能够监控节点状态并负责选主。

  2. 同级节点
    :在没有第三方协调者的情况下,主备结构的节点会通过彼此的协调进行选主,通常是由Follower节点通过一定的算法(如Paxos或Raft)选出新的Leader。

第三方协调者,Zookeeper如何选主?

Zookeeper通过心跳机制(会话超时检测)来检测每个服务节点的运行状态。启动时,多个服务节点会在Zookeeper上按顺序生成临时顺序节点,序号最小的成为Leader节点。每个节点会监听前一个节点的状态,如果前一个节点挂掉(即它的临时节点被删除),当前节点就会变成Leader节点,并通知其他Follower节点进行同步。

这种选举方案并没有传统意义上的“投票”过程,只是通过顺序节点的编号自动接替,最小编号的节点成为Leader。

//官网描述//https://zookeeper.apache.org/doc/current/recipes.html
Leader Election
A simple way of doing leader election
with ZooKeeper is to use the SEQUENCE|EPHEMERAL flags when creating znodes that represent "proposals" of clients. The idea is to have a znode, say "/election", such that each znode creates a child znode "/election/guid-n_" with both flags SEQUENCE|EPHEMERAL. With the sequence flag, ZooKeeper automatically appends a sequence number that is greater than anyone previously appended to a child of "/election". The process that created the znode withthe smallest appended sequence number is the leader.

That
's not all, though. It is important to watch for failures of the leader, so that a new client arises as the new leader in the case the current leader fails. A trivial solution is to have all application processes watching upon the current smallest znode, and checking if they are the new leader when the smallest znode goes away (note that the smallest znode will go away if the leader fails because the node is ephemeral). But this causes a herd effect: upon a failure of the current leader, all other processes receive a notification, and execute getChildren on "/election" to obtain the current list of children of "/election". If the number of clients is large, it causes a spike on the number of operations that ZooKeeper servers have to process. To avoid the herd effect, it is sufficient to watch for the next znode down on the sequence of znodes. If a client receives a notification that the znode it is watching is gone, then it becomes the new leader in the case that there is no smaller znode. Note that this avoids the herd effect by not having all clients watching the same znode.

问题就是,接替上来的Follower节点,其数据未必是最新的

一种变体:利用Zookeeper选出控制器

Kafka利用Zookeeper选出控制器(Controller),然后由Controller来控制分区首领的选举。它可以进一步判断分区副本的同步偏移量,来选出数据最新的副本作为分区首领。

基于第三方协调者的实现虽然简单,但存在单点故障问题。zookeeper如何挂掉,就无法及时选出主节点。

同级节点:Raft协议如何选主?

Raft 协议的选主过程不依赖第三方协调者,而是由服务节点之间通过互相通信来进行决策。Raft 选主的过程涉及几个关键问题:

1. Follower节点如何确定Leader节点是否正常?

Raft 协议通过
心跳机制
(通常是 AppendEntries RPC)来保持 Leader 节点的存活状态。Leader 节点定期向 Follower 节点发送心跳信号。如果 Follower 节点在一定时间内没有收到 Leader 的心跳信号,它会认为 Leader 节点可能已挂掉,进而发起选举。

2. Leader节点挂掉后,如何选主?

当 Leader 节点挂掉或无法继续提供服务时,Follower 节点会开始
投票
来选举新的 Leader:

  • Follower节点如何投票
    :每个 Follower 节点会
    为自己拉票
    ,它会请求其他节点投票支持自己成为新的 Leader。Raft 协议中,每个节点都有一个
    任期
    (term),每个选举周期都会有一个新的任期号。节点通过比较任期号来判断是否接受对方的投票。

  • 投票规则
    :在选举过程中,票数最多的节点成为新的 Leader。每个节点只能投一票,而且该票只能投给一个任期号大于自己当前任期号的节点。
    得票数超过半数的节点
    成为 Leader。

关于“投票”

  1. Follower节点如何知道总共有哪些Follower节点?

    Follower 节点通过 Leader 节点获得集群中其他节点的信息。Leader 节点会定期通过心跳(AppendEntries RPC)向 Follower 节点传递集群成员的信息。Follower 节点在没有 Leader 时,会基于之前的集群信息来进行选举。

  2. Follower节点如何拉票?

    Follower 节点通过
    请求投票
    (RequestVote RPC)向其他节点请求支持。每个 Follower 节点会在自己的选举任期内发起投票请求,要求其他节点为自己投票。如果请求节点的任期更大,并且数据更新,被请求方就会同意投票请求。

  3. Follower节点如何确定自己票数最多?

    Follower 节点并不需要知道其他节点的具体得票情况。Raft 协议保证,当一个节点获得大多数(过半数)节点的支持时,它会成为 Leader。在选举过程中,节点只需要确保自己获得过半数的选票,而不需要知道其他节点的具体得票情况。

3. 原Leader节点恢复运行后怎么办?是否会出现两个Leader节点?

Raft 协议通过
任期号
(Term)来防止出现两个 Leader 节点。当原 Leader 节点恢复运行时,它会发现自己所属的任期号已经过期(即其他节点已选出了新的 Leader),因此它会自动变成 Follower 节点,并接受新的 Leader 的控制。

  • 任期号
    :每次选举时,都会增加任期号。所有节点都会跟踪当前的任期号,任期号大的节点会被视为合法的 Leader。当原 Leader 节点恢复时,由于它的任期号较旧,它会识别出自己已不再是 Leader,从而转变为 Follower。

Raft协议是否解决拜占庭问题?

没有。可以这样说,
Raft 协议
的前提假设是集群中的节点是由可信方部署和控制的,且这些节点会按照协议规则运行,不存在执行恶意操作的情况。


总结

主备架构不仅仅应用于数据库和缓存系统,很多微服务架构、消息队列和分布式存储系统都需要通过主备结构来保证系统的高可用性。随着技术的进步,现代的主备结构逐渐引入了更智能的选主机制,确保即使在极端条件下也能迅速恢复服务。

前言

最近在捣鼓代码生成器,基于 Roslyn,我们可以让生成器项目生成我们的目标 C# 代码,这个也是
MVVM Toolkit
的实现方式,在查看生成代码的过程中,我们经常会遇到一些特殊的特性,如
GeneratedCodeAttribute
,刚好我还遇到过
CompilerGeneratedAttribute
。感觉两个特性差不多,都可以用于标识代码的生成来源,帮助开发者和其他工具更好地理解和处理代码。

GeneratedCodeAttribute
解析

定义与用途

GeneratedCodeAttribute
是一个系统提供的特性,定义在
System.CodeDom.Compiler
命名空间,用于标记由工具或编译器生成的代码。它通常包含两个参数:生成工具的名称和版本号。

[AttributeUsage(AttributeTargets.All, Inherited = false)]
public sealed class GeneratedCodeAttribute : Attribute
{
    public string Tool { get; }
    public string Version { get; }

    public GeneratedCodeAttribute(string tool, string version)
    {
        Tool = tool;
        Version = version;
    }
}

这个特性的主要用途是:

  • 标识生成的代码
    :当你使用 Source Generator、T4 模板、Roslyn API 或其他代码生成工具时,可以在生成的文件中添加这个特性,以明确指出代码是由哪个工具生成的。
  • 避免误修改
    :标记为
    GeneratedCode
    的代码可以提醒开发者不要直接编辑这些文件,因为它们是自动生成的,任何手动修改可能会在下次生成时丢失。
  • 分析器和工具支持
    :某些分析器(如 Roslyn 分析器)和工具会识别并特殊处理带有
    GeneratedCodeAttribute
    的代码,例如忽略代码覆盖率统计或特定的代码分析规则。

示例

假设你有一个 Source Generator 工具名为
MyCustomTool
,版本为
1.0.0
,你可以这样标记生成的代码:

[GeneratedCode("MyCustomTool", "1.0.0")]
public partial class MyClass
{
    // 自动生成的代码
}

CompilerGeneratedAttribute
解析

定义与用途

CompilerGeneratedAttribute
定义在
System.Runtime.CompilerServices
命名空间,是一个更具体的特性,用于标记由 C# 编译器自动生成的代码片段。它没有参数,仅表示代码是由编译器生成的。

[AttributeUsage(AttributeTargets.All, Inherited = false)]
public sealed class CompilerGeneratedAttribute : Attribute
{
}

这个特性的主要用途是:

  • 标识编译器生成的代码
    :当编译器为了实现某些语言特性(如匿名类型、迭代器、异步方法等)而自动生成代码时,会自动添加这个特性。这有助于区分用户编写的代码和编译器生成的代码。
  • 内部实现细节
    :这个特性主要用于内部实现细节,普通开发者通常不需要手动添加它。它是编译器用来标记其生成的代码的一种方式。

示例

编译器生成的代码片段可能如下所示:

[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
    public int x;

    internal void <Method>b__0()
    {
        Console.WriteLine(x);
    }
}

区别与选择

虽然
GeneratedCodeAttribute

CompilerGeneratedAttribute
都用于标识代码的生成来源,但它们有着不同的用途和适用场景。

  • 来源不同


    • GeneratedCodeAttribute
      通常由外部工具或源代码生成器添加,以标识代码是由某个工具生成的,一般来说是出于编码人员的自身目标。
    • CompilerGeneratedAttribute
      由 C# 编译器自身添加,用于标识编译器生成的代码片段。
  • 应用场景


    • 如果你正在开发 Source Generator 或其他代码生成工具,并希望标记生成的代码以便后续处理或提醒开发者不要直接编辑这些文件,应该手动标记使用
      GeneratedCodeAttribute
    • 不应该手动添加该特性,如果你在查看编译后的代码,发现带有
      CompilerGeneratedAttribute
      的类或成员,这通常是编译器为了实现某些语言特性而生成的代码,不应被手动修改。

代码生成器应用示例

MVVM Toolkit 就按照这个标准开发,假设你正在开发一个 Source Generator 来生成部分类文件:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.CodeDom.Compiler;

public class MySourceGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Initialization logic if needed
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var sourceBuilder = new StringBuilder();
        sourceBuilder.AppendLine("[GeneratedCode(\"MyCustomTool\", \"1.0.0\")]");
        sourceBuilder.AppendLine("public partial class MyClass");
        sourceBuilder.AppendLine("{");
        sourceBuilder.AppendLine("    public string MyProperty { get; set; }");
        sourceBuilder.AppendLine("}");

        context.AddSource("MyClass.g.cs", sourceBuilder.ToString());
    }
}

上面代码中,
GeneratedCodeAttribute
被用来标记生成的代码,确保其他工具和开发者知道这段代码是由
MyCustomTool
生成的。

一些建议:

  • 不适用于用户可修改的模板
    :如果有一个代码生成工具生成的模板,用户可能会根据需要对其进行修改,那么就不应该使用
    GeneratedCodeAttribute
    标记这些模板。因为一旦代码被手动修改,再用
    GeneratedCodeAttribute
    标记就不再准确了,而且可能会误导其他工具忽略这些手动修改的内容。
  • 部分类的特殊处理
    :当生成的代码是部分类的一部分时,不要在整个类上应用
    GeneratedCodeAttribute
    。相反,你应该仅将此特性应用于该部分类中生成的具体成员(如方法、字段、属性等)。这是因为部分类可以有多个文件定义,而用户可能在其他文件中添加自己的实现。通过只标记生成的成员,你可以确保只有自动生成的部分被正确标识,而不会影响用户添加的代码。(这个可以看 MVVM Toolkit 生成的代码)

总结

简单说来:

  • GeneratedCodeAttribute
    主要用于标记由工具或编译器生成的代码,特别是那些会频繁重新生成的代码。这有助于开发者和其他工具识别这些代码片段,并避免对它们进行不必要的修改。
  • CompilerGeneratedAttribute
    一般不要手动添加到代码中。

参考文献