Spanner技术分析

Spanner技术分析Spanner 的论文已经公布几个月了 技术网站上已经有不少的分析文章 为了更深入的从工程角度理解 Spanner 我最近又重新对论文中设计的关键技术细节进行了分析 主要的参考仍然来自于 GoogleResear 公布的 OSDI2012 上的 Spanner 论文以及演讲材料 本文的重点是介绍 Spa

Spanner的论文已经公布几个月了,技术网站上已经有不少的分析文章。为了更深入的从工程角度理解Spanner,我最近又重新对论文中设计的关键技术细节进行了分析。主要的参考仍然来自于Google Research公布的OSDI 2012上的Spanner论文以及演讲材料。本文的重点是介绍Spanner的TrueTime和分布式事务两项核心功能

Spanner

1 Spanner模型

1.1 部署模型

Spanner有一套复杂的数据部署模型,以下是论文中描述的一些关键单元

  • directory(目录),是具有连续键值的数据集合
  • fragment(目录片),是对directory按键值区间的切分,目的是防止directory过大,是指定部署分布的基本单元
  • tablet,通过元数据进行管理多个不连续的directory,是副本管理的基本单元,即同一tablet中的数据具有相同的副本分布配置
  • server,即spanner的物理服务器,存储多个tablet
  • group,副本组,包含分布在多个server上的具有相同元数据的tablet,通过paxos达到数据一致。
  • zone,是由同一数据中心中服务器组成的小范围集群,用于配置不同业务的物理隔离
  • data center,数据中心,可看作集中部署的多个zone的物理集合
  • universe,zone的逻辑集合,可能跨多个data center,Google使用universe分隔测试环境和线上环境

spanner deployment

其中directory和fragment是由数据模型逻辑自然产生的(参见1.2节),Spanner使用后台进程(Movedir)不断重新调整directory和fragment在tablet中的部署,目的是使频繁同时查询的数据尽可能聚合在一起,因此一个tablet的元数据是可能发生变化的。

Spanner的另一套机制决定group中的tablet如何在server上分布,这套机制考虑的通常是全球容灾和用户访问延时问题,例如约束某个表的数据在北美保存2个备份、亚洲2份、欧洲1份。

1.2 数据模型

Spanner使用一种有Schema的嵌套数据模型,同时使用类SQL的语法作为访问接口,用论文中的例子来解释

spanner nested data

上面的两个SQL语句看似创建了两张表:Users和Albums,但实际上Spanner将它们合并存储。注意第二张表的建表语句使用了多主键:uid和aid,并且通过“INTERLEAVE IN PARENT …”语句表示uid与Users中的主键uid一致。这意味着,Album中的多条uid相同的记录,是嵌套在Users表的一条uid记录之下的。同一个user之下的所有子数据,就构成了一个directory。论文并没有描述表中的非主键如何存储,因此只能推测它们存储在所对应主键之后,即如下结构:

spanner data model

使用嵌套的数据结构,Spanner实现了相关联数据的合并存储,并且对于前面部署模型提到的后台directory迁移机制十分友好,属于同一Users下的所有记录可以同时取出。另外对于需要频繁使用两表主键join的操作,这种模型也能够大幅降低磁盘IO开销。论文中并没有具体说明,这样的嵌套模型是否具有层次深度是否仅限于2层,即是否能创建另一张表interleave in parent Albums。不过这在技术上并没有什么障碍。

关于存储模型,Spanner论文并没有给出具体的说明。考虑到时间戳和多版本的设定,个人推测Spanner采用了与BigTable和LevelDB类似的增量模型(毕竟它们都是由Jeffery Deam等几位大牛牵头的),该模型的特征是数据以增量的方式写入到存储介质,而非在原有存储空间上进行修改。至于如何基于增量模型实现嵌套型数据存储,也并不是一件简单的事情,开源NoSQL数据库还没有一个类似的产品,以后有机会专门写文章详细介绍。

2 TrueTime

2.1 分布式事务和时序

全球分布式和一致性事务,在现有架构下通常很难共存。前者要求读操作可以在任何一个节点进行(通常会选择里客户端最近的节点),后者要求无论客户端链接到哪个节点,读取的数据都是一致的。我们列举2个场景说明同时实现两个要求是如何困难,以及为什么这些困难都与时序问题有关。

场景1:主备一致

主备一致性困难

上图是主备一致问题的一个具体示例,客户端向Master节点提交(A=2)更新后,转向Slave节点查询A的值,此时Slave可能尚未写入更新的数据。要解决这个问题有两个思路:

  1. 强制要求写入是同步备份的
  2. 对Slave节点的读操作,能够判断数据是否足够新,否则阻塞读操作

已有的数据库经验表明,实现第一个思路的代价是巨大的。因为:1)有些的数据库写操作的代价较大,Slave节点强制同步写入会导致写延时增加。2)主备节点的连接并不可靠(全球部署和备份的系统更是如此),Master向Slave两阶段提交的任何时刻都可能发生故障。

而第二个思路,需要解决的问题是,数据写入和查询的新旧顺序如何确定?进一步,Master、Slave、Client三个分布式节点如何统一的描述一个具有顺序的逻辑?

场景2:跨库(或分区)事务原子性

跨库事务困难

上图是跨库事务原子性问题的一个示例,Client 1节点提交一个写事务,向两个跨库或分区的Master节点分别提交对记录A和B的写操作。同时Client 2节点希望查询A和B的值。

在传统方法中,Client 2 的对A、B的读取必须封装到一个读事务中。这是因为:由于缺乏分布式的时序,Master 1和Master 2对数据的加锁必须是有次序的,如果来自Client 2的两个读请求不尝试也获得锁,它们就无法保证这两个读操作都发生在写事务之前或之后。有些场景下,读事务对性能可能造成严重影响。假设该模型描述的是一个商品数据,A和B是同一个商品的两个属性,但分布在两个表中,Client 1代表了商品的成交,Client 2则代表商品信息的查询。当查询的频率远高于成交操作,成交操作的性能将受到严重影响。

分布式数据中的Schema变更操作,可能更能说明跨库(分区)中的事务操作多么依赖分布式锁。传统分布式数据库需要对所有有关表加DDL锁,阻塞读写操作。在没有时序逻辑的系统中,一次全局加锁操作的时间代价是巨大的。就是说一旦需要更新Schema,传统分布式数据库就必须忍受一次长时间的全网业务中断。

2.2 TrueTime的技术基础

TrueTime是一套保持Spanner全球服务器的时钟“近似同步”的基础服务。它基于已有的全球精准计时技术:GPS时钟和原子钟。Spanner同时使用两种校时技术是有原因的。解释这个问题,需要先简单介绍两种时钟的原理:

  • 原子钟利用原子的波长测量时间,科学领域通常使用最精确的铯原子时钟,但成本也很高。而商用领域则使用精度和成本都较低的铷原子钟。无论哪种原子钟,都存在误差累积问题,即原子钟自然产生的误差是单调变化的,两个不同的原子钟授时差异会越来越大。
  • GPS时钟的技术基础,仍然是每个GPS卫星上的两个互相校时的原子钟。GPS时钟终端可以通过连接多颗GPS卫星,通过算法屏蔽电磁波传输时延计算出相对精确时间。因此GPS时钟产生的误差是随机误差,即全球不同GPS时钟的时间虽然会呈现动态不一致,但误差不会越来越大。然而,GPS毕竟是第三方服务,并不能确信它在任何情况下都可用(例如发生大范围的卫星故障、电磁干扰等)。

综合以上原因,Spanner决定同时采用GPS时钟和原子钟,并且以GPS时钟为主。这样可以既避免计时误差问题,又保证了GPS失效时的可用性。 此外Google在论文中也透露了,原子钟与GPS时钟的成本在相同数量级,所以成本问题并不是Spanner选择的一个关键因素。

既然是工程角度的探讨,就需要再引申介绍一下,如何搭建基于GPS时钟(原子钟)的服务器。目前的精确校时商用设备,按授时的物理接口可分为以下几类:

  • RS232,即9 pin口,现在的主机已经很少有这种接口了。
  • PCI,插到服务器主板上即可,缺点是需要为每台服务器配置一个。
  • 以太网,使用PTP/PTPv2(IEEE1558)作为校时协议,目前的商用设备的精度已经能够达到0.1us

可以简单推测Spanner采用的是以太网PTP方式进行的网络校时,形成如下组网架构:

time master

2.3 Time Master服务

Spanner的每个数据中心里都有一定数量的时钟服务器,其中多数是GPS时钟服务、少数是原子钟服务,它们被统称为time master。提供数据服务的主机称为spanner server。spanner server会周期性的选择若干time master进行网络校时。

  • 同时链接不同距离的数据中心的多个time master,计算时间参考值
  • 使用防欺骗算法,侦测和过滤撒谎的time master
  • 若频繁出现本地时钟在校时周期内产生的误差过大,则主动退出Spanner服务

spanner server会周期性的将本地时钟调整到全球一致的值(时钟本身和校时产生的误差范围内)。但在相邻校时周期中间,它完全依赖本地时钟读取时间,在此期间由于本地时钟的不精确性,从而积累误差。Google提供的经验值是,一般情况下本地时钟每秒积累的误差不会超过200us,相当于1/5000的误差。以此经验值为前提,TrueTime可以通过调整spanner server的校时周期,保持这种误差小于一个特定的值。例如Spanner的周期设定为30秒,误差上限值被设置为7ms(大于30*0.2ms)。这个值被成为”误差参考值“,记作ε

可以简单的这样理解,Spanner保证每个服务器在任何时刻获取的时间,与“全球标准时间”相差不超过7ms。或者说任意两个Spanner服务器之间的时间误差不超过14ms。后面的分析会说明,这个最大误差值越小,Spanner的事务性能将越高。Google在论文中也提到,缩短校时周期可以有效改善误差时间,个人推测目前使用的30秒周期可能是PTP服务本身的并发性能限制有关,因此增加Time Master的数量可以间接缩短该误差值。

由于定义了误差参考值ε,就可以定义TrueTime API三个接口

  • now()
    • 返回一个时间区间(tearliest, tlatest),使得tearliest

      abs

      earliest

    • 论文中没有具体描述上下界的计算方法,显然若定义tearliest=tlocal-ε,tlatest=tlocal+ε是满足要求的。
  • after(t)
    • 当确定当前全球精确时间大于输入的时间记录时返回true,否则返回false
  • before(t)
    • 当确定当前全球精确时间大于输入的时间记录时返回true,否则返回false

举一个例子描述如何使用TrueTime控制分布式系统的时序,要求”节点A上执行事件e1的绝对时间早于节点B上执行事件e2“

不使用TrueTime API的方法如下

  1. A执行e1
  2. A通知B
  3. B执行e2

使用TrueTime的方法如下:

  1. A、B协商时间戳t1、t2,其中t2-t1>2ε
  2. A根据本地时间在t1执行e1
  3. B根据本地时间在t2执行e2

证明:用tabs(e)表示时间e的执行绝对时间,根据误差参考值定义,tabs(e1) < t1+ε, tabs(e2) > t2-ε,又因为t2-t1>2ε,所以tabs(e1)

abs(e2)

3 分布式事务

3.1 时间戳

Spanner是一个具有多版本特征的数据库。它的每个最小粒度的数据都具有一个时间戳属性。时间戳在数据写入时生成,并且遵循以下规则:

  1. 大于等于写操作起始绝对时间 ,即ts>=t(eserver),其中eserver是节点接收到写操作的事件
  2. 小于等于写操作commit绝对时间,即ts<=t(ecommit)),其中ecommit是写操作提交commit的事件
  3. 同一次写事务生成的时间戳保持一致

3.2 快照读和只读事务

Spanner读写操作的类型包括三种:快照读、只读事务、读写事务。

快照读是对历史数据的查询,的流程如下:

  1. 客户端指定一个时间戳ts
  2. 根据读请求和数据分布信息,选择与本次查询有关的副本组
  3. 客户端在每个group选择一个副本节点,分别发起查询请求,等待至全部完成
  4. 在每个所选副本节点上:
    1. 阻塞流程,直到时间戳ts同时满足以下条件
      • 小于该节点在所有正在执行的读写事务中产生的时间戳(参见2.2节步骤6.2)
      • 小于该节点paxos同步时间戳(参见2.3节)
    2. 执行读操作,执行成功后返回客户端,否则重试直到超时

快照读使用时间戳对数据进行筛选,客户端可以指定(过去、当前或未来)任意时刻时间戳进行查询,因此查询结果与请求提交的时间无关。Spanner具备查询过去某个版本数据的能力。

快照读引入了读等待所机制(步骤4.1),副本节点达到足够新的状态之后才完成读取,因此即使数据复制是异步执行的,也可以保证跨副本节点的读写一致性。正是由于快照读的事务一致性与副本选择无关,步骤1.2可以根据业务需要自行制定副本选择策略,不限于以下几种:

  • 优先选择ping最低的节点,保证通信延时最小化
  • 优先选择负载最轻的节点,保证多副本节点负载均衡
  • 优先选择leader节点,保证读流程阻塞最小化

只读事务针对客户端没有指定时间戳的情况,默认含义是查询当前时刻数据,具体流程如下:

  1. 选择查询节点,同快照读步骤1
  2. 客户端判断读操作是否只在一个group内部
  • 如果是
  1. 客户端请求该group的Leader节点,发起查询请求并等待完成
  2. Leader节点将读操作时间戳ts指定为LastTS

如果否

  1. 客户端将读操作时间戳ts指定为now.latest
  2. 客户端在每个group选择一个副本节点,分别发起查询请求,等待至全部完成,同快照度步骤3

被查询的节点执行读操作并返回,同快照读步骤4

只读事务区分读操作是否在一个group的意义在于,面向多节点读操作需要统一时间戳,这种情况下,由客户端指定时间戳的消息通信代价是最小的。而对于只需要在一个节点上执行的读操作,LastTS(接受消息时刻最后的写事务时间戳)会比now.last要小,特别是该tablet最近一段时间没有写入数据的情况,Spanner针对这种场景将生成时间戳的权利交给Leader节点,能够很大概率降低读阻塞时间。

由于spanner服务端节点引入了读阻塞机制,只读事务和快照读不需要使用锁。

spanner read

3.3 读写事务

读写事务的流程如下:

  1. 获取相关Leader节点的读锁
  2. 如果需要读操作,快照读(仅选择leader节点)
  3. 客户端确定写操作的所有副本组,选择一个coodinator-leader
    • 论文并没有给出选择方法,因此流程对任意的选择方法都是兼容的。
    • 如果客户端只确定了一个副本组,则该副本组的leader即为coodinator-leader,以下流程跳过步骤5.2和6
  4. 客户端生成需要写入的数据,分别发送给所选择副本组的leader节点(消息中包含所选coodinator-leader的信息)
  5. coodinator-leader节点执行以下操作:
    1. 获取写锁
    2. 等待所有非coordinator-leader的消息
    3. 确定本次事务最终的时间戳ts,遵循以下三个规则
      • 大于所有其他leader消息中的时间戳
      • 大于收到它收到客户端消息时的now().latest(为了满足时间戳规则1)
      • 大于本节点所有已使用的时间戳
    4. 将客户端提交的数据,通过paxos写入到副本binlog日志
    5. commit等待:阻塞流程直到after(ts)==true(为了满足时间戳规则2)
    6. 本地并通知slave节点提交commit
    7. 释放写锁
    8. 通知客户端写操作完成,同时通知非coordinator-leader进行commit
  6. 非coodinator-leader节点执行以下操作:
    1. 获取写锁
    2. 确定准备时间戳,大于本节点所有已使用的时间戳
    3. 将客户端提交的数据,通过paxos写入到副本binlog日志
    4. 通知coordinator-leader
    5. 等待coordinator-leader的消息
    6. 讲binlog时间戳改为coordinator-leader确定的时间戳ts(为了满足时间戳规则3)
    7. 本地并通知slave节点提交commit
    8. 释放写锁

spanner write

Spanner论文对读写事务中锁的描述比较简略,遗留了许多问题需要填补:

  • 锁的粒度是如何的?
    • 论文暗示锁粒度小于tablet大于单行记录,但具体是directory、fragment或者其它粒度,文中并没有具体解释,我们暂时按粒度为fragment理解
  • 如何选择加锁对象?
    • (基于推测)客户端会分析读写操作,判断本次操作涉及哪些主键区间,然后计算出所对应Leader节点上的fragment,然后加锁。注意步骤1的读锁和步骤5、6中的写锁加锁对象可能是不同的,但加读锁的fragment集合必须包含加写锁的fragment集合。
  • 死锁问题?
    • Spanner论文中提到了使用”wound-wait“(受伤-等待)策略防范死锁。简单回顾wound-wait策略:1)当较新的任务尝试获得锁,等待直到锁释放。2)当较老的任务尝试获得锁,结束较新任务。(基于推测)Spanner使用读写事务中读操作的时间戳比较任务的新旧。因此当客户端尝试为分布在多个节点上的fragment加锁时,可通过本次时间戳比较任务的新旧,从而决定该读写事务是否等待或这终止。
  • 使用读写锁,如何保障事务的隔离性?
    • 回顾以下读写锁机制:1)读锁之间不互斥、2)读锁与写锁,以及写锁之间互斥。对于仅仅是简单的读写锁方案,如下图a所示,当线程1和线程2先后启动读写操作且并行执行,在第一阶段两个线程同时获得了读锁,读到相同版本的数据,第二阶段由于写锁互斥先后执行。线程2的写操作出现了“脏读”,将了线程1的数据,破坏了事务的隔离性?
    • 不过wound-wait策略除了解决死锁之外,实际上该机制也保证了前面描述的脏读现象不会发生。如下图b所示。当线程2完成读操作尝试获得写锁,此时线程1仍然持有写锁,因此进入等待。当线程1开始写操作,尝试获得写锁,线程2由于任务较新则直接终止了任务(并不是终止线程)。

spanner read-write lock

读写事务的步骤5.4和6.3是一个复合步骤,涉及Leader节点与Slave节点之间交互,参见3.5节。

3.4 Leader租约

在一个group中tablet备份分布在不同节点上,其中一个称为Leader节点,其余称为Slave节点,它们都称为副本节点(replica)。为了降低开销,Spanner采用Multi-Paxos协议进行数据备份。但这个协议会产生一个问题,当Leader节点需要重新选举的时候,可能多个节点同时认为自身是Leader,这将破坏Spanner读写事务中的其它一些设定。因此Spanner必须作出更严格的约束,任何时刻只有一个节点认为自己是Leader。

为了保证异常状况下的Leader及时重新选举,以及避免选举造成过重的开销,Spanner采用有租约的Leader管理机制,时长默认是10秒,即一个节点确认获得授权之后10秒可以认为自己是Leader节点。为了避免Leader节点闪断后重新回到group时出现新旧Leader共存,在一个Leader租约时间内,重新选举新的Leader也是不允许的。另一个问题是,不同节点对于租约起止时刻的判断误差,也可能造成短暂的新旧Leader共存,Spanner使用TrueTime API避免这一点。具体流程如下:

spanner leader vote

  1. 一个Replica试图成为Leader时,向所有replica节点r发送租约请求,每次消息发送之前调用TrueTime,记录tleader,r=now().latest
  2. 当节点r接收到租约请求消息,判断上一次发出的租约投票是否超期,如果未超期则流程结束,否则执行步骤3
  3. 返回一个租约投票,并记录租约起止时间为now().latest到now().latest+10秒
  4. 当发起租约请求的节点接收到多数replica节点的租约投票,则成为leader,它所记录的租约时间起止时间为now().latest到minr(tleader,r)+10秒,并使用before(minr(tleader,r)+10)判断是安全的判断当前时刻是否在租约之内。

上述流程中,不同节点维护的租约时间不同,而leader节点则取了大多数节点租约投票时间的交集,从而保证不与前后产生的leader节点产生租约时间的重叠。具体的证明可以参见原文附录A。

Spanner还引入了租约延长机制,进一步避免leader重新选举代价,包含显式和隐式两种方法(具体的流程并未说明):

  • 当发生一次写操作,隐式延长租约
  • 租约即将结束时,显式延长租约

注意到Leader节点延长租约所需的多数节点并不需要是固定的,也就是说当它拥有任意多数节点的租约投票,即可延长Leader租约。

另外,Leader选举流程可能存在活锁问题,即多个replica节点同时试图成为leader时,由于没有一个达到多数replica投票,流程反复启动和终止。特别是步骤2要求投票后的租约时间内不再进行投票,产生一次活锁将导致10秒内Leader无法选举产生。不过虽然论文中没有提及,相信Spanner已经采用了其它机制规避该问题。

3.5 Paxos数据复制

Spanner Leader利用Paxos进行binlog数据复制,每个paxos实例中决议的对象是下一条binlog记录,流程如下:

  1. Leader将数据写入自身的binlog
  2. Leader将数据发送给Slave
  3. 接收到消息的Slave将数据写入自身的binlog,并返回消息
  4. Leader等待直到超过半数的Slave返回

spanner paxos

上述数据复制过程仅仅保证binlog数据的主备一致性,因此不涉及数据commit和commit wait,它们交由读写事务流程完成。

3.6 Schema变更

Spanner的Schema变更事务流程如下:

  1. 确定一个未来时间戳t,显式大于所有server的收到并确认执行的时间
  2. 将schema事务发送到所有节点,
  3. 每个节点根据schema事务,生成新版本的schema(与旧的schema并存),按照如下方式执行读写操作
    • 对于时间戳小于t的读写操作,使用就版本schema执行
    • 对于时间戳大于等于t的读写操作,阻塞直到需要after(t)为true,然后使用新的schema执行

Spanner不再需要接收到DDL操作之后开始逐步阻塞所有节点上的读写操作,直到确认所有节点进入阻塞状态再确认执行schema变更。TrueTime保证了,在绝对时刻t所有节点都处于阻塞读写操作状态,而小于2ε时间之后所有节点自行恢复读写,从而将节点阻塞读写操作的时间降至最低。

3.7 一些探讨

Spanner哪些地方使用了TrueTime?

  • 读操作:保证本次读操作时间戳小于当前正在写的时间戳
  • 写操作:leader提交commit之前,保证commit的绝对时刻大于写操作时间戳
  • Leader租约选举:保证新旧leader的租约不发生重叠

关于时间戳规则2和commit等待真的必要吗?

4 技术对比

(待补充)

  • Oracle RAC
  • IBM PureScale
  • MegaStore

转载于:https://my.oschina.net/whchsh/blog/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/215792.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午1:09
下一篇 2026年3月18日 下午1:09


相关推荐

  • echarts中国地图配置

    echarts中国地图配置scripttype text javascript src js echarts min js scripttype text javascript src js china js functionrand returnMath round Math random 500 varmydata name 北京 value scripttype text scripttype text

    2026年3月26日
    2
  • pycharm虚拟环境下安装第三方库_pycharm需要配置环境变量吗

    pycharm虚拟环境下安装第三方库_pycharm需要配置环境变量吗pycharm配置虚拟环境安装虚拟环境1.安装相关库pipinstallvirtualenv2.切换到python安装目录下,创建虚拟环境virtualenv虚拟环境名(可自定义)virtualenvvenv3.进入cd到虚拟环境的位置(目录)的Scripts中,激活(activate.bat)虚拟环境cdvenv\Scripts#激活虚拟环境activate.bat4.退出虚拟环境deactivate.batdeactivate.bat5.使用在p

    2022年8月28日
    10
  • c++ 线程间通信方式「建议收藏」

    c++ 线程间通信方式「建议收藏」线程同步和线程互斥互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的,线程间不需要知道彼此的存在。同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问,线程间知道彼此的存在。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源线程…

    2022年10月7日
    5
  • Python爬虫实战——搭建自己的IP代理池[通俗易懂]

    Python爬虫实战——搭建自己的IP代理池[通俗易懂]如今爬虫越来越多,一些网站网站加强反爬措施,其中最为常见的就是限制IP,对于爬虫爱好者来说,能有一个属于自己的IP代理池,在爬虫的道路上会减少很多麻烦环境参数工具详情服务器Ubuntu编辑器Pycharm第三方库requests、bs4、redis搭建背景之前用Scrapy写了个抓取新闻网站的项目,今天突然发现有一个网站的内容爬不下来…

    2022年5月11日
    59
  • windows服务器性能监控工具、方法及关键指标

    windows服务器性能监控工具、方法及关键指标监控方法推荐使用 windows 自带的 性能监视器 老版本的 windows 叫性能计数器 来监控服务器的性能 打开控制面板内的管理工具 在管理工具内打开性能监视器 出现如下界面 各版本的 window 操作系统的性能监视器的界面可能略有不同 点击中上部的绿色加号图标 可以添加一项监视内容 添加界面如下图所示 可以在左侧选中需要监控的内容 点击添

    2026年3月26日
    2
  • Fuel 5.1安装openstack I版本号环境

    Fuel 5.1安装openstack I版本号环境

    2022年1月3日
    45

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号