基于Patroni的Citus高可用环境部署

1. 前言

Citus是一个非常实用的能够使PostgreSQL具有进行水平扩展能力的插件,或者说是一款以PostgreSQL插件形式部署的基于PostgreSQL的分布式HTAP数据库。本文简单说明Citus的高可用技术方案,并实际演示基于Patroni搭建Citus HA环境的步骤。

2. 技术方案

2.1 Citus HA方案选型

Citus集群由一个CN节点和N个Worker节点组成。CN节点的高可用可以使用任何通用的PG 高可用方案,即为CN节点通过流复制配置主备2台PG机器;Worker节点的高可用除了可以像CN一样采用PG原生的高可用方案,还支持另一种多副本分片的高可用方案。

多副本高可用方案是Citus早期版本默认的Worker高可用方案(当时citus.shard_count默认值为2),这种方案部署非常简单,而且坏一个Worker节点也不影响业务。采用多副本高可用方案时,每次写入数据,CN节点需要在2个Worker上分别写数据,这也带来一系列不利的地方。

  1. 数据写入的性能下降
  2. 对多个副本的数据一致性的保障也没有PG原生的流复制强
  3. 存在功能上的限制,比如不支持Citus MX架构

因此,Citus的多副本高可用方案适用场景有限,Citus 官方文档上也说可能它只适用于append only的业务场景,不作为推荐的高可用方案了(在Citus 6.1的时候,citus.shard_count默认值从2改成了1)。

因此,建议Citus和CN和Worker节点都使用PG的原生流复制部署高可用。

2.2 PG HA支持工具的选型

PG本身提供的流复制的HA的部署和维护都不算很复杂,但是如果我们追求更高程度的自动化,特别是自动故障切换,可以使用一些使用第3方的HA工具。目前有很多种可选的开源工具,下面几种算是比较常用的

  • PAF(PostgreSQL Automatic Failover)
  • repmgr
  • Patroni

它们的比较可以参考: Managing PostgreSQL High Availability Pt.1: Automatic Failover

其中Patroni采用DCS(Distributed Configuration Store,比如etcd,ZooKeeper,Consul等)存储元数据,能够严格的保障元数据的一致性,可靠性高;而且它的功能也比较强大。

因此个人推荐使用Patroni(只有2台机器无法部署etcd的情况可以考虑其它方案)。本文介绍基于Patroni的PostgreSQL高可用的部署。

2.3 客户端流量切换方案

PG 主备切换后,访问数据库的客户端也要相应地连接到新的主库。目前常见的有下面几种方案:

  • HAProxy

    • 优点

      • 可靠
      • 支持负载均衡
    • 缺点

      • 性能损耗
      • 需要配置HAProxy自身的HA
  • VIP

    • 优点
      • 无性能损耗,不占用机器资源
    • 缺点
      • 主备节点IP必须在同网段
  • 客户端多主机URL

    • 优点

      • 无性能损耗,不占用机器资源
      • 不依赖VIP,易于在云环境部署
      • pgjdbc支持读写分离和负载均衡
    • 缺点

      • 仅部分客户端驱动支持(目前包括pgjdbc,libpq和基于libpq的驱动,如python和php)
      • 如果数据库层面没控制好出现了"双主", 客户端同时向2个主写数据的风险较高

根据Citus集群的特点,推荐的候选方案如下

  • 应用连接Citus
    • 客户端多主机URL

      如果客户端驱动支持,特别对Java应用,推荐采用客户端多主机URL访问Citus

    • VIP

  • Citus CN连接Worker
    • VIP
    • Worker节点发生切换时动态修改Citus CN上的worker节点元数据

关于Citus CN连接Worker的方式,本文下面的实验中会演示2种架构,采用不同的实现方式。

普通架构

  • CN通过Worker的实际IP连接Worekr主节点
  • CN上通过监控脚本检测Worker节点状态,Worker发生主备切换时动态修改Citus CN上的元数据

支持读写分离的架构

  • CN通过Worker的读写VIP和只读VIP连接Worekr
  • CN上通过Patroni回调脚本动态控制CN主节点使用读写VIP,CN备节点使用只读VIP
  • Worker上通过Patroni回调脚本动态绑定读写VIP
  • Worker上通过keepalived动态绑定只读VIP

3. 实验环境

主要软件

  • CentOS 7.8
  • PostgreSQL 12
  • Citus 10.4
  • patroni 1.6.5
  • etcd 3.3.25

机器和VIP资源

  • Citus CN
    • node1:192.168.234.201
    • node2:192.168.234.202
  • Citus Worker
    • node3:192.168.234.203
    • node4:192.168.234.204
  • etcd
    • node4:192.168.234.204
  • VIP(Citus CN )
    • 读写VIP:192.168.234.210
    • 只读VIP:192.168.234.211

环境准备

所有节点设置时钟同步

yum install -y ntpdate
ntpdate time.windows.com && hwclock -w

如果使用防火墙需要开放postgres,etcd和patroni的端口。

  • postgres:5432
  • patroni:8008
  • etcd:2379/2380

更简单的做法是将防火墙关闭

setenforce 0
sed -i.bak "s/SELINUX=enforcing/SELINUX=permissive/g" /etc/selinux/config
systemctl disable firewalld.service
systemctl stop firewalld.service
iptables -F

4. etcd部署

因为本文的主题不是etcd的高可用,所以只在node4上部署单节点的etcd用于实验。生产环境至少需要3台独立的机器,也可以和数据库部署在一起。etcd的部署步骤如下

安装需要的包

yum install -y gcc python-devel epel-release

安装etcd

yum install -y etcd

编辑etcd配置文件/etc/etcd/etcd.conf, 参考配置如下

ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.234.204:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://192.168.234.204:2379"
ETCD_NAME="etcd0"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.234.204:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.234.204:2379"
ETCD_INITIAL_CLUSTER="etcd0=http://192.168.234.204:2380"
ETCD_INITIAL_CLUSTER_TOKEN="cluster1"
ETCD_INITIAL_CLUSTER_STATE="new"

启动etcd

systemctl start etcd

设置etcd自启动

systemctl enable etcd

5. PostgreSQL + Citus + Patroni HA部署

在需要运行PostgreSQL的实例上安装相关软件

安装PostgreSQL 12和Citus

yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpmyum install -y postgresql12-server postgresql12-contrib
yum install -y citus_12

安装Patroni

yum install -y gcc epel-release
yum install -y python-pip python-psycopg2 python-develpip install --upgrade pip
pip install --upgrade setuptools
pip install patroni[etcd]

创建PostgreSQL数据目录

mkdir -p /pgsql/data
chown postgres:postgres -R /pgsql
chmod -R 700 /pgsql/data

创建Partoni的service配置文件/etc/systemd/system/patroni.service

[Unit]
Description=Runners to orchestrate a high-availability PostgreSQL
After=syslog.target network.target[Service]
Type=simple
User=postgres
Group=postgres
#StandardOutput=syslog
ExecStart=/usr/bin/patroni /etc/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=no[Install]
WantedBy=multi-user.target

创建Patroni配置文件/etc/patroni.yml,以下是node1的配置示例

scope: cn
namespace: /service/
name: pg1restapi:listen: 0.0.0.0:8008connect_address: 192.168.234.201:8008etcd:host: 192.168.234.204:2379bootstrap:dcs:ttl: 30loop_wait: 10retry_timeout: 10maximum_lag_on_failover: 1048576master_start_timeout: 300synchronous_mode: falsepostgresql:use_pg_rewind: trueuse_slots: trueparameters:listen_addresses: "0.0.0.0"port: 5432wal_level: logicalhot_standby: "on"wal_keep_segments: 1000max_wal_senders: 10max_replication_slots: 10wal_log_hints: "on"max_connections: "100"max_prepared_transactions: "100"shared_preload_libraries: "citus"citus.node_conninfo: "sslmode=prefer"citus.replication_model: streamingcitus.task_assignment_policy: round-robininitdb:- encoding: UTF8- locale: C- lc-ctype: zh_CN.UTF-8- data-checksumspg_hba:- host replication repl 0.0.0.0/0 md5- host all all 0.0.0.0/0 md5postgresql:listen: 0.0.0.0:5432connect_address: 192.168.234.201:5432data_dir: /pgsql/databin_dir: /usr/pgsql-12/binauthentication:replication:username: replpassword: "123456"superuser:username: postgrespassword: "123456"basebackup:max-rate: 100Mcheckpoint: fasttags:nofailover: falsenoloadbalance: falseclonefrom: falsenosync: false

其他PG节点的patroni.yml需要相应修改下面4个参数

  • scope
    • node1,node2设置为cn
    • node3,node4设置为wk1
  • name
    • node1~node4分别设置pg1~pg4
  • restapi.connect_address
    • 根据各自节点IP设置
  • postgresql.connect_address
    • 根据各自节点IP设置

启动Patroni

在所有节点上启动Patroni。

systemctl start patroni

同一个cluster中,第一次启动的Patroni实例会作为leader运行,并初始创建PostgreSQL实例和用户。后续节点初次启动时从leader节点克隆数据

查看cn集群状态

[root@node1 ~]# patronictl -c /etc/patroni.yml list
+ Cluster: cn (6869267831456178056) +---------+----+-----------+-----------------+
| Member |       Host      |  Role  |  State  | TL | Lag in MB | Pending restart |
+--------+-----------------+--------+---------+----+-----------+-----------------+
|  pg1   | 192.168.234.201 |        | running |  1 |       0.0 |        *        |
|  pg2   | 192.168.234.202 | Leader | running |  1 |           |                 |
+--------+-----------------+--------+---------+----+-----------+-----------------+

查看wk1集群状态

[root@node3 ~]# patronictl -c /etc/patroni.yml list
+ Cluster: wk1 (6869267726994446390) ---------+----+-----------+-----------------+
| Member |       Host      |  Role  |  State  | TL | Lag in MB | Pending restart |
+--------+-----------------+--------+---------+----+-----------+-----------------+
|  pg3   | 192.168.234.203 |        | running |  1 |       0.0 |        *        |
|  pg4   | 192.168.234.204 | Leader | running |  1 |           |                 |
+--------+-----------------+--------+---------+----+-----------+-----------------+

为了方便日常操作,设置全局环境变量PATRONICTL_CONFIG_FILE

echo 'export PATRONICTL_CONFIG_FILE=/etc/patroni.yml' >/etc/profile.d/patroni.sh

添加以下环境变量到~postgres/.bash_profile

export PGDATA=/pgsql/data
export PATH=/usr/pgsql-12/bin:$PATH

设置postgres拥有sudoer权限

echo 'postgres        ALL=(ALL)       NOPASSWD: ALL'> /etc/sudoers.d/postgres

5. 配置Citus

在cn和wk的主节点上创建citus扩展

create extension citus

在cn的主节点上,添加wk1的主节点IP,groupid设置为1。

SELECT * from master_add_node('192.168.234.204', 5432, 1, 'primary');

在Worker的主备节点上分别修改/pgsql/data/pg_hba.conf配置文件,以下内容添加到其它配置项前面允许CN免密连接Worker。

host all all 192.168.234.201/32 trust
host all all 192.168.234.202/32 trust

修改后重新加载配置

su - postgres
pg_ctl reload

注:也可以通过在CN上设置~postgres/.pgpass 实现免密,但是没有上面的方式维护方便。

创建分片表测试验证

create table tb1(id int primary key,c1 text);
set citus.shard_count = 64;
select create_distributed_table('tb1','id');
select * from tb1;

6. 配置Worker的自动流量切换

上面配置的Worker IP是当时的Worker主节点IP,在Worker发生主备切换后,需要相应更新这个IP。

实现上,可以通过脚本监视Worker主备状态,当Worker主备角色变更时,自动更新Citus上的Worker元数据为新主节点的IP。下面是脚本的参考实现

将以下配置添加到Citus CN主备节点的/etc/patroni.yml

citus:loop_wait: 10databases:- postgresworkers:- groupid: 1nodes:- 192.168.234.203:5432- 192.168.234.204:5432

也可以使用独立的配置文件,如果那样做需要补充认证配置

postgresql:connect_address: 192.168.234.202:5432authentication:superuser:username: postgrespassword: "123456"

创建worker流量自动切换脚本/pgsql/citus_controller.py

#!/usr/bin/env python2
# -*- coding: utf-8 -*-import os
import time
import argparse
import logging
import yaml
import psycopg2def get_pg_role(url):result = 'unknow'try:with psycopg2.connect(url, connect_timeout=2) as conn:conn.autocommit = Truecur = conn.cursor()cur.execute("select pg_is_in_recovery()")row = cur.fetchone()if row[0] == True:result = 'secondary'elif row[0] == False:result = 'primary'except Exception as e:logging.debug('get_pg_role() failed. url:{0} error:{1}'.format(url, str(e)))return resultdef update_worker(url, role, groupid, nodename, nodeport):logging.debug('call update worker. role:{0} groupid:{1} nodename:{2} nodeport:{3}'.format(role, groupid, nodename, nodeport))try:sql = "select nodeid,nodename,nodeport from pg_dist_node where groupid={0} and noderole = '{1}' order by nodeid limit 1".format(groupid, role)conn = psycopg2.connect(url, connect_timeout=2)conn.autocommit = Truecur = conn.cursor()cur.execute(sql)row = cur.fetchone()if row is None:logging.error("can not found nodeid whose groupid={0} noderole = '{1}'".format(groupid, role))return Falsenodeid = row[0]oldnodename = row[1]oldnodeport = str(row[2])if oldnodename == nodename and oldnodeport == nodeport:logging.debug('skip for current nodename:nodeport is same')return Falsesql= "select master_update_node({0}, '{1}', {2})".format(nodeid, nodename, nodeport)ret = cur.execute(sql)logging.info("Changed worker node {0} from '{1}:{2}' to '{3}:{4}'".format(nodeid, oldnodename, oldnodeport, nodename, nodeport))return Trueexcept Exception as e:logging.error('update_worker() failed. role:{0} groupid:{1} nodename:{2} nodeport:{3} error:{4}'.format(role, groupid, nodename, nodeport, str(e)))return Falsedef main():parser = argparse.ArgumentParser(description='Script to auto setup Citus worker')parser.add_argument('-c', '--config', default='citus_controller.yml')parser.add_argument('-d', '--debug', action='store_true', default=False)args = parser.parse_args()if args.debug:logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.DEBUG)else:logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)# read config filef = open(args.config,'r')contents = f.read()config = yaml.load(contents, Loader=yaml.FullLoader)cn_connect_address = config['postgresql']['connect_address']username = config['postgresql']['authentication']['superuser']['username']password = config['postgresql']['authentication']['superuser']['password']databases = config['citus']['databases']workers = config['citus']['workers']loop_wait = config['citus'].get('loop_wait',10)logging.info('start main loop')loop_count = 0while True:loop_count += 1logging.debug("##### main loop start [{}] #####".format(loop_count))dbname = databases[0]cn_url = "postgres://{0}/{1}?user={2}&password={3}".format(cn_connect_address,dbname,username,password)if(get_pg_role(cn_url) == 'primary'):for worker in workers:groupid = worker['groupid']nodes = worker['nodes']## get role of worker nodesprimarys = []secondarys = []for node in nodes:wk_url = "postgres://{0}/{1}?user={2}&password={3}".format(node,dbname,username,password)role = get_pg_role(wk_url)if role == 'primary':primarys.append(node) elif role == 'secondary':secondarys.append(node) logging.debug('Role info groupid:{0} primarys:{1} secondarys:{2}'.format(groupid,primarys,secondarys))## update worker nodefor dbname in databases:cn_url = "postgres://{0}/{1}?user={2}&password={3}".format(cn_connect_address,dbname,username,password)if len(primarys) == 1:nodename = primarys[0].split(':')[0]nodeport = primarys[0].split(':')[1]update_worker(cn_url, 'primary', groupid, nodename, nodeport)"""Citus的pg_dist_node元数据中要求nodename:nodeport必须唯一,所以无法同时支持secondary节点的动态更新。一个可能的回避方法是为每个worker配置2个IP地址,一个作为parimary角色时使用,另一个作为secondary角色时使用。if len(secondarys) >= 1:nodename = secondarys[0].split(':')[0]nodeport = secondarys[0].split(':')[1]update_worker(cn_url, 'secondary', groupid, nodename, nodeport)elif len(secondarys) == 0 and len(primarys) == 1:nodename = primarys[0].split(':')[0]nodeport = primarys[0].split(':')[1]update_worker(cn_url, 'secondary', groupid, nodename, nodeport)"""time.sleep(loop_wait)if __name__ == '__main__':main()

创建该脚本的service配置文件/etc/systemd/system/citus_controller.service

[Unit]
Description=Auto update primary worker ip in Citus CN
After=syslog.target network.target[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/bin/python /pgsql/citus_controller.py -c /etc/patroni.yml
KillMode=process
TimeoutSec=30
Restart=no[Install]
WantedBy=multi-user.target

在cn主备节点上都启动Worker流量自动切换脚本

systemctl start citus_controller

7. 读写分离

根据上面的配置,Citus CN不会访问Worker的备机,这些备机闲着也是闲着,能否把这些备节用起来,让Citus CN支持读写分离呢?具体而言就是让CN的备机优先访问Worker的备机,Worker备节故障时访问Worker的主机。

Citus本身支持读写分离功能,可以把一个Worker的主备2个节点作为2个”worker"分别以primarysecondary的角色加入到同一个worker group里。但是,由于Citus的pg_dist_node元数据中要求nodename:nodeport必须唯一,所以前面的动态修改Citus元数据中的worker IP的方式无法同时支持primary节点和secondary节点的动态更新。

解决办法有2个

方法1:Citus元数据中只写固定的主机名,比如wk1,wk2...,然后通过自定义的Worker流量自动切换脚本将这个固定的主机名解析成不同的IP地址写入到/etc/hosts里,在CN主库上解析成Worker主库的IP,在CN备库上解析成Worker备库的IP。

方法2:在Worker上动态绑定读写VIP和只读VIP。在Citus元数据中读写VIP作为primary角色的worker,只读VIP作为secondary角色的worker。

Patroni动态绑VIP的方法参考基于Patroni的PostgreSQL高可用环境部署.md,对Citus Worker,读写VIP通过回调脚本动态绑定;只读VIP通过keepalived动态绑定。

下面按方法2进行配置。

创建Citus集群时,在CN的主节点上,添加wk1的读写VIP(192.168.234.210)和只读VIP(192.168.234.211),分别作为primary worker和secondary worker,groupid设置为1。

SELECT * from master_add_node('192.168.234.210', 5432, 1, 'primary');
SELECT * from master_add_node('192.168.234.211', 5432, 1, 'secondary');

为了让CN备库连接到secondary的worker,还需要在CN备库上设置以下参数

alter system set citus.use_secondary_nodes=always;
select pg_reload_conf();

这个参数的变更只对新创建的会话生效,如果希望立即生效,需要在修改参数后杀掉已有会话。

现在分别到CN主库和备库上执行同一条SQL,可以看到SQL被发往不同的worker。

CN主库(未设置citus.use_secondary_nodes=always):

postgres=# explain select * from tb1;QUERY PLAN
-------------------------------------------------------------------------------Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=100000 width=36)Task Count: 32Tasks Shown: One of 32->  TaskNode: host=192.168.234.210 port=5432 dbname=postgres->  Seq Scan on tb1_102168 tb1  (cost=0.00..22.70 rows=1270 width=36)
(6 rows)

CN备库(设置了citus.use_secondary_nodes=always):

postgres=# explain select * from tb1;QUERY PLAN
-------------------------------------------------------------------------------Custom Scan (Citus Adaptive)  (cost=0.00..0.00 rows=100000 width=36)Task Count: 32Tasks Shown: One of 32->  TaskNode: host=192.168.234.211 port=5432 dbname=postgres->  Seq Scan on tb1_102168 tb1  (cost=0.00..22.70 rows=1270 width=36)
(6 rows)

由于CN也会发生主备切换,``citus.use_secondary_nodes`参数必须动态调节。这可以使用Patroni的回调脚本实现

创建动态设置参数的/pgsql/switch_use_secondary_nodes.sh

#!/bin/bashDBNAME=postgres
KILL_ALL_SQL="select pg_terminate_backend(pid) from pg_stat_activity  where backend_type='client backend' and application_name <> 'Patroni' and pid <> pg_backend_pid()"action=$1
role=$2
cluster=$3log()
{echo "switch_use_secondary_nodes: $*"|logger
}alter_use_secondary_nodes()
{value="$1"oldvalue=`psql -d postgres -Atc "show citus.use_secondary_nodes"`if [ "$value" = "$oldvalue" ] ; thenlog "old value of use_secondary_nodes already be '${value}', skip change"returnfipsql -d ${DBNAME} -c "alter system set citus.use_secondary_nodes=${value}" >/dev/nullrc=$?if [ $rc -ne 0 ] ;thenlog "fail to alter use_secondary_nodes to '${value}' rc=$rc"exit 1fipsql -d ${DBNAME} -c 'select pg_reload_conf()' >/dev/nullrc=$?if [ $rc -ne 0 ] ;thenlog "fail to call pg_reload_conf() rc=$rc"exit 1filog "changed use_secondary_nodes to '${value}'"## kill all existing connectionskilled_conns=`psql -d ${DBNAME} -Atc "${KILL_ALL_SQL}" | wc -l`rc=$?if [ $rc -ne 0 ] ;thenlog "failed to kill connections rc=$rc"exit 1filog "killed ${killed_conns} connections"}log "switch_use_secondary_nodes start args:'$*'"case $action inon_start|on_restart|on_role_change)case $role inmaster)alter_use_secondary_nodes never;;replica)alter_use_secondary_nodes always;;*)log "wrong role '$role'"exit 1;;esac;;*)log "wrong action '$action'"exit 1;;
esac

修改Patroni配置文件/etc/patroni.yml,配置回调函数

postgresql:
...callbacks:on_start: /bin/bash /pgsql/switch_use_secondary_nodes.shon_restart: /bin/bash /pgsql/switch_use_secondary_nodes.shon_role_change: /bin/bash /pgsql/switch_use_secondary_nodes.sh

所有节点的Patroni配置文件都修改后,重新加载Patroni配置

patronictl reload cn

CN上执行switchover后,可以看到use_secondary_nodes参数发生了修改

/var/log/messages:

Sep 10 00:10:25 node2 postgres: switch_use_secondary_nodes: switch_use_secondary_nodes start args:'on_role_change replica cn'
Sep 10 00:10:25 node2 postgres: switch_use_secondary_nodes: changed use_secondary_nodes to 'always'
Sep 10 00:10:25 node2 postgres: switch_use_secondary_nodes: killed 0 connections

8. 参考

  • 基于Patroni的PostgreSQL高可用环境部署.md
  • 《基于PatroniCitus高可用方案》(PostgreSQL中国用户大会2019分享主题)
  • Introduction — Patroni 3.3.1 documentation
  • CentOS 7 + PostgreSQL 10 + Patroni – unixwiz
  • Managing PostgreSQL High Availability Pt.1: Automatic Failover
  • https://jdbc.postgresql.org/documentation/use/
  • Seamless Application Failover using libpq Features in PostgreSQL

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/35308.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

U盘提示格式化怎么搞定?本文有5种方法(内含教程)

U盘提示格式化是一种常见故障&#xff0c;即&#xff1a;当U盘插入电脑后&#xff0c;电脑上弹出对话框&#xff0c;提示该U盘需要格式化才能使用。 接触不良、文件系统损坏、热插拔、感染病毒、芯片损坏等原因都可能导致U盘出现此故障。这时点击“格式化”&#xff0c;大概率会…

Vim使用教程

1.为啥用Vim 一个字帅&#xff0c;但是我这个属于自己的笔记&#xff0c;写的水平不高&#xff0c;不懂得可以评论。用的是windows上的nvim 2.常用模式 普通模式&#xff0c;就是阅读模式插入模式&#xff0c;常用i/a/o键可进入&#xff0c;就是编辑模式。可视化模式&#xff…

蒸汽架空管道中的关键守护者:滑动管托、导向管托与固定管托

蒸汽架空管道中的关键守护者&#xff1a;滑动管托、导向管托、固定管托与补偿器的重要角色在蒸汽架空管道系统中&#xff0c;每一个组件都扮演着不可或缺的角色&#xff0c;共同确保管道的安全、高效运行。今天&#xff0c;我们就来深入探讨滑动管托、导向管托、固定管托以及补…

武汉星起航:深度洞察消费趋势,亚马逊美国站选品独具匠心

亚马逊美国站作为全球电商巨头的重要分支&#xff0c;其选品特点不仅反映了美国市场的消费趋势&#xff0c;更引领着全球消费者的购物潮流。从运动户外、宠物用品到美容个人护理&#xff0c;亚马逊美国站的选品策略始终紧跟市场脉搏&#xff0c;为消费者提供丰富多样、品质优良…

Go实现SFTP客户端

我将提供一个封装了 SFTP 客户端操作的简单例子&#xff0c;以确保通用性。这个例子包括连接、断开连接、上传文件、下载文件、列出目录和创建目录的基本操作。 这个 SFTPClient 结构体包含了一个 *sftp.Client 字段&#xff0c;我们通过实现方法来提供基本的 SFTP 操作。NewC…

简化收支记录,只留关键日期! 一键掌握财务流动,高效管理您的每一笔收支

在繁忙的生活中&#xff0c;管理个人或家庭的财务收支变得尤为重要。然而&#xff0c;传统的记账方式往往繁琐且复杂&#xff0c;让人望而却步。今天&#xff0c;我们为您推荐一款简洁易用的记账神器——晨曦记账本&#xff0c;让您轻松记录收支&#xff0c;只显示日期&#xf…

揭秘!这款电路设计工具让学校师生都爱不释手——SmartEDA的魔力何在?

随着科技的飞速发展&#xff0c;电子设计已成为学校师生们不可或缺的技能之一。而在众多的电路设计工具中&#xff0c;有一款名为SmartEDA的工具&#xff0c;凭借其强大的功能和友好的用户体验&#xff0c;迅速赢得了广大师生的青睐。今天&#xff0c;就让我们一起探索SmartEDA…

Leetcode TOP5 题目和解答:这里只提供一种解题思路,希望引导大家持续学习,可以采用FlowUs息流记录自己的学习

LeetCode 是一个在线编程平台&#xff0c;它提供了大量的算法题目供用户练习。 TOP5题目通常指的是 LeetCode 网站上最受欢迎的前5道题目。 以下是 LeetCode TOP5 题目的列表以及它们常见的解题思路和代码示例。 题目1 两数之和 两数之和 - 1. Two Sum Given an array of int…

html5 video去除边框

video的属性&#xff1a; autoplay 视频在就绪后自动播放。 controls 显示控件&#xff0c;比如播放按钮。 height 设置视频播放器的高度。 width 设置视频播放器的宽度。 loop 循环播放 muted 视频的音频输出静音。 poster 视频加载时显示的图像&#xff0c;或者在用户点击播…

短视频利器 ffmpeg (2)

ffmpeg 官网这样写到 Converting video and audio has never been so easy. 如何轻松简单的使用&#xff1a; 1、下载 官网&#xff1a;http://www.ffmpeg.org 安装参考文档&#xff1a; https://blog.csdn.net/qq_36765018/article/details/139067654 2、安装 # 启用RPM …

clonezilla(再生龙)克隆物理机linux系统,然后再去另一台电脑安装

前言: 总共需要2个u盘,一个装再生龙系统,一个是使用再生龙把硬盘备份到另一个盘里面,恢复的时候,先使用再生龙引导,然后再插上盘进行复制 1.制作启动u盘 1.1下载再生龙Clonezilla 下載 1.2下载UltraISO(https://cn.ultraiso.net/uiso9_cn.exe) 1.3 打开UltraISO,选择co…

聚类模型的算法性能评价

一、概述 作为机器学习领域的重要内容之一&#xff0c;聚类模型在许多方面能够发挥举足轻重的作用。所谓聚类&#xff0c;就是通过一定的技术方法将一堆数据样本依照其特性划分为不同的簇类&#xff0c;使得同一个簇内的样本有着更相近的属性。依不同的实现策略&#xff0c;聚类…

g++、make、cmake三种方式来编译ros2的C++节点

上面我们用g、make、cmake三种方式来编译ros2的C节点。用cmake虽然成功了&#xff0c;但是CMakeLists.txt的内容依然非常的臃肿&#xff0c;我们需要将其进一步的简化 ROS2前置基础教程 | 小鱼教你用CMake依赖查找流程_ros2cmakelist-CSDN博客

力扣哈希刷题——总结篇【三】

前言 哈希篇题目完成&#xff0c;学到那些方法呢&#xff1f;回顾一下。 方法 &#xff08;1&#xff09;“判断一个元素有没有在某个集合中出现过”——可以考虑哈希结构。 &#xff08;2&#xff09;哈希结构&#xff1a;数组&#xff1f;set&#xff1f;map&#xff1f;选…

华为HCIP Datacom H12-821 卷16

1.判断题 在 VRRP 中,当设备状态变为 Master 后,,会立刻发送免费 ARP 来刷新下游设备的 MAC 表项,从而把用户的流量引到此台设备上来 A、对 B、错 正确答案: A 解析: 2.判断题 路由选择工具 route- policy 能够基于预先定义的条件来进行过滤并设置 BGP

canvas画布旋转问题

先说一下为什么要旋转的目的&#xff1a;因为在画布上签名&#xff0c;在不同的设备上我需要不同方向的签名图片&#xff0c;电脑是横屏&#xff0c;手机就是竖屏&#xff0c;所以需要把手机的签名旋转270&#xff0c;因此写了这个方法。 关于画布旋转的重点就是获取到你的画布…

typescript 基本类型

基本类型注解 任意类型 let value: any ;string类型 let message: string hello world;number类型 let orderid: number 0;booblean类型 let openDiolog: boolean false;字符串类型数组 let teachers: string[]; const students: string[] [张, 王];数字类型数组 c…

软件著作权的申请信息在哪看?

软著对于企业来说是一个非常有价值的知识产权。软著可以保证企业自身的利益得到合法的保护&#xff0c;并且可以反映企业的技术创新能力&#xff0c;能够让企业提高自己的竞争力&#xff0c;在申报一些补贴&#xff0c;招标时作为加分项。因此&#xff0c;很多科技型企业都会申…

1982Springboot宠物美容院管理系统idea开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 springboot宠物美容院管理系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系 统具有完整的源代码和数据库…

DNS的工作原理

DNS的工作原理 DNS&#xff08;Domain Name System&#xff09;是一个分布式数据库系统&#xff0c;负责将人类可读的域名转换为互联网上计算机可以理解的IP地址。其工作原理大致分为以下几个步骤&#xff1a; 用户在浏览器或应用中输入域名&#xff1a;当用户键入一个网站地…