XRDP 和 NVIDIA 显卡兼容性问题
背景
最近在尝试配置 XRDP,发现它在有 NVIDIA 的机器上启动远程桌面后会黑屏,查看错误信息可以看到:
解决方法
XRDP 作者在 issue #2010 中提到了解决方法:
修改 /etc/xrdp/sesman.ini,在 [Xorg]
部分里加上下面的配置:
实际上就是不让 Xorg 加载 nvidia xorg 驱动,这样就绕过了问题。
最近在尝试配置 XRDP,发现它在有 NVIDIA 的机器上启动远程桌面后会黑屏,查看错误信息可以看到:
XRDP 作者在 issue #2010 中提到了解决方法:
修改 /etc/xrdp/sesman.ini,在 [Xorg]
部分里加上下面的配置:
实际上就是不让 Xorg 加载 nvidia xorg 驱动,这样就绕过了问题。
之前和 NVIDIA 驱动和 CUDA 搏斗比较多,因此记录一下一些常用信息,方便查询。
可以通过 apt show cuda-runtime-x-x 找到:
使用 nvidia-smi 看到的 CUDA 版本,通常就是这个驱动在上表里对应的 CUDA 版本,例如内核驱动版本是 470 的话,看到的 CUDA 版本就是 11.4。
实际上兼容的驱动版本会比 APT 宣称的更多一些:官方文档 里面写了 CUDA 11.x 可以兼容 NVIDIA >= 450,CUDA 12.x 可以兼容 NVIDIA >= 525。
可以在 cuda/include/crt/host_config.h 文件里找到:
编译选项与显卡对应关系 https://arnon.dk/matching-sm-architectures-arch-and-gencode-for-various-nvidia-cards/
可以在 nvcc --help
搜索 gpu-architecture 找到:
显卡的 Compute Capability 可以在 https://developer.nvidia.com/cuda-gpus 找到:
升级后,需要 rmmod 已有的,再 modprobe 新的:
如果发现 rmmod 失败,可以 lsof /dev/nvidiactl
查看谁在占用。DGX OS 上需要停止:
除了 /dev/nvidia*
可能被占用以外,还需要用 lsof 检查 /dev/dri/render*
。
本文的内容已经整合到知识库中。
最近在《高等计算机系统结构》课程中学习缓存一致性协议算法,这里用自己的语言来组织一下相关知识的讲解。
最基础的缓存一致性思想有两种:
Write-once 协议定义了四个状态:
可见,当一个缓存状态在 R 或者 D,其他缓存只能是 I;而缓存状态是 V 的时候,可以有多个缓存在 V 状态。
Write-once 协议的特点是,第一次写的时候,会写入到内存(类似 Write-through),连续写入则只写到缓存中,类似 Write-back。
当 Read hit 的时候,状态不变。
Read hit: The information is supplied by the current cache. No state change.
当 Read miss 的时候,会查看所有缓存,如果有其他缓存处于 Valid/Reserved/Dirty 状态,就从其他缓存处读取数据,然后设为 Valid,其他缓存也设为 Valid。如果其他缓存处于 Dirty 状态,还要把数据写入内存。
Read miss: The data is read from main memory. The read is snooped by other caches; if any of them have the line in the Dirty state, the read is interrupted long enough to write the data back to memory before it is allowed to continue. Any copies in the Dirty or Reserved states are set to the Valid state.
当 Write hit 的时候,如果是 Valid 状态,首先写入内存,把其他 Cache 都设为 Invalid,进入 Reserved 状态,这意味着第一次写是 Write-through。如果是 Reserved/Dirty 状态,则不修改内存,进入 Dirty 状态,这表示后续的写入都是 Write-back。
Write hit: If the information in the cache is in Dirty or Reserved state, the cache line is updated in place and its state is set to Dirty without updating memory. If the information is in Valid state, a write-through operation is executed updating the block and the memory and the block state is changed to Reserved. Other caches snoop the write and set their copies to Invalid.
当 Write miss 的时候,这个行为 Wikipedia 上和上课讲的不一样。按照 Wikipedia 的说法,首先按照 Read miss 处理,再按照 Write hit 处理,类似于 Write Allocate 的思路。如果是这样的话,那么首先从其他缓存或者内存读取数据,然后把其他缓存都设为 Invalid,把更新后的数据写入内存,进入 Reserved 状态。相当于 Write miss 的时候,也是按照 Write-through 实现。
Write miss: A partial cache line write is handled as a read miss (if necessary to fetch the unwritten portion of the cache line) followed by a write hit. This leaves all other caches in the Invalid state, and the current cache in the Reserved state.
教材上则是 Write miss 的时候按照 Write-back 处理。如果其他缓存都是 Invalid 时,从内存里读取数据,然后写入到缓存中,进入 Dirty 状态。如果其他缓存是 Valid/Reserved/Dirty 状态,就从其他缓存里读取数据,让其他缓存都进入 Invalid 状态,然后更新自己的数据,进入 Dirty 状态。
MSI 协议比较简单,它定义了三个状态:
当 Read hit 的时候,状态不变。
当 Read miss 的时候,检查其他缓存的状态,如果都是 Invalid,就从内存里读取,然后进入 Shared 状态。如果有 Shared,就从其他缓存处读取。如果有 Dirty,那就要把其他缓存的数据写入内存和本地缓存,然后进入 Shared 状态。
当 Write hit 的时候,如果现在是 Shared 状态,则要让其他的 Shared 缓存进入 Invalid 状态,然后更新数据,进入 Modified 状态。如果是 Modified 状态,那就修改数据,状态保持不变。
当 Write miss 的时候,如果有其他缓存处于 Modified/Shared 状态,那就从其他缓存处读取数据,并让其他缓存进入 Invalid 状态,然后修改本地数据,进入 Modified 状态。如果所有缓存都是 Invalid 状态,那就从内存读入,然后修改缓存数据,进入 Modified 状态。
MESI 协议定义了四种状态:
当 Read hit 的时候,状态不变。
当 Read miss 的时候,首先会检查其他缓存的状态,如果有数据,就从其他缓存读取数据,并且都进入 Shared 状态,如果其他缓存处于 Modified 状态,还需要把数据写入内存;如果其他缓存都没有数据,就从内存里读取,然后进入 Exclusive 状态。
当 Write hit 的时候,进入 Modified 状态,同时让其他缓存进入 Invalid 状态。
当 Write miss 的时候,检查其他缓存的状态,如果有数据,就从其他缓存读取,否则从内存读取。然后,其他缓存都进入 Invalid 状态,本地缓存更新数据,进入 Modified 状态。
值得一提的是,Shared 状态不一定表示只有一个缓存有数据:比如本来有两个缓存都是 Shared 状态,然后其中一个因为缓存替换变成了 Invalid,那么另一个是不会受到通知变成 Exclusive 的。Exclusive 的设置是为了减少一些总线请求,比如当数据只有一个核心访问的时候,只有第一次 Read miss 会发送总线请求,之后一直在 Exclusive/Modified 状态中,不需要发送总线请求。
MOESI 定义了五个状态:
状态中,M 和 E 是独占的,所有缓存里只能有一个。此外,可以同时有多个 S,或者多个 S 加一个 O,但是不能同时有多个 O。
它的状态转移与 MESI 类似,区别在于:当核心写入 Owned 状态的缓存时,有两种方式:1)通知其他 Shared 的缓存更新数据;2)把其他 Shared 缓存设为 Invalid,然后本地缓存进入 Modified 状态。在 Read miss 的时候,则可以从 Owned 缓存读取数据,进入 Shared 状态,而不用写入内存。它相比 MESI 的好处是,减少了写回内存的次数。
AMD64 文档里采用的就是 MOESI 协议。AMBA ACE 协议其实也是 MOESI 协议,只不过换了一些名称,表示可以兼容 MEI/MESI/MOESI 中的一个协议。ACE 对应关系如下:
需要注意的是,SharedClean 并不代表它的数据和内存一致,比如说和 SharedDirty 缓存一致,它只是说缓存替换的时候,不需要写回内存。
Dragon 协议是一个基于更新的协议,意味着写入缓存的时候,会把更新的数据同步到拥有这个缓存行的其他核心。它定义了四个状态:
可以看到,E 和 M 都是独占的,如果出现了多个缓存有同一个缓存行,那就是若干个 Sc 和一个 Sm。
当 Read miss 的时候,在总线上检查是否有缓存已经有这个缓存行的数据,如果没有,则从内存读取并转到 Exclusive clean 状态;如果已经在其他缓存中,则从其他缓存读取,将其他缓存转移到 Shared clean/Shared modified 状态,然后该缓存转移到 Shared clean 状态。
当 Write miss 的时候,同样检查其他缓存的状态,如果是第一个访问的,就从内存读取,更新数据,然后转到 Modify 状态;如果不是第一个访问的,就进入 Shared modified 状态,并且让原来 Shared modified 的缓存进入 Shared clean 状态。
当 Write hit 的时候,如果状态是 Shared modified,这时候需要通知其他缓存更新数据;如果状态是 Shared clean,则要通知其他缓存更新数据的同时,让原来 Shared modified 的缓存进入 Shared clean 状态;如果状态是 Exclusive clean,则进入 Modify 状态。
在这里,Shared modified 的缓存负责在换出的时候,写入数据到内存中。
ACE 协议在 AXI 的基础上,添加了三个 channel:
此外,已有的 Channel 也添加了信号:
ACE-lite 只在已有 Channel 上添加了新信号,没有添加新的 Channel。因此它内部不能有 Cache,但是可以访问一致的缓存内容。
当 Read miss 的时候,首先 AXI master 发送 read transaction 给 Interconnect,Interconnect 向保存了这个缓存行的缓存发送 AC 请求,如果有其他 master 提供了数据,就向请求的 master 返回数据;如果没有其他 master 提供数据,则向内存发起读请求,并把结果返回给 master,最后 master 提供 RACK 信号。
当 Write miss 的时候,也是类似地,AXI master 发送 MakeUnique 请求给 Interconnect,Interconnect 向保存了该缓存行的缓存发送请求,要求其他 master 状态改为 Invalid;当所有 master 都已经 invalidate 成功,就向原 AXI master 返回结果。
上面的缓存一致性协议中,经常有这么一个操作:向所有有这个缓存行的缓存发送/接受消息。简单的方法是直接广播,然后接受端自己判断是否处理。但是这个方法在核心很多的时候会导致广播流量太大,因此需要先保存下来哪些缓存会有这个缓存的信息,然后对这些缓存点对点地发送。这样就可以节省一些网络流量。
那么,怎么记录这个信息呢?一个简单的办法(Full bit vector format)是,有一个全局的表,对每个缓存行,都记录一个大小为 N(N 为核心数)的位向量,1 表示对应的核心中有这个缓存行。但这个方法保存数据量太大:缓存行数正比于 N,还要再乘以一次 N,总容量是 O(N^2) 的。
一个稍微好一些的方法(Coarse bit vector format)是,我把核心分组,比如按照 NUMA 节点进行划分,此时每个缓存行都保存一个大小为 M(M 为 NUMA 数量)的位向量,只要这个 NUMA 节点里有这个缓存行,对应位就取 1。这样相当于是以牺牲一部分流量为代价(NUMA 节点内部广播),来节省一些目录的存储空间。
但实际上,通常情况下,一个缓存行通常只会在很少的核心中保存,所以这里有很大的优化空间。比如说,可以设置一个缓存行同时出现的缓存数量上限 (Limited pointer format),然后保存核心的下标而不是位向量,这样的存储空间就是 O(Nlog2N)。但是呢,这样限制了缓存行同时出现的次数,如果超过了上限,需要替换掉已有的缓存,可能在一些场景下性能会降低。
还有一种方式,就是链表 (Chained directory format)。目录中保存最后一次访问的核心编号,然后每个核心的缓存里,保存了下一个保存了这个缓存行的核心编号,或者表示链表终止。这样存储空间也是 O(Nlog2N),不过发送消息的延迟更长,因为要串行遍历一遍,而不能同时发送。类似地,可以用二叉树 (Number-balanced binary tree format) 来组织:每个缓存保存两个指针,指向左子树和右子树,然后分别遍历,目的还是加快遍历的速度,可以同时发送消息给多个核心。
最近我们设计的 Kintex 7 FPGA 开发板在测试 DDR SDRAM 的时候遇到了一个问题,因为采用了 Internel VREF,MIG 在配置的时候限制了频率只能是 400 MHz,对应 800 MT/s,这样无法达到 DDR 的最好性能。
首先,VREF 在 DDR 中是用来区分低电平和高电平的。在 JESD79-4B 标准中,可以看到,对于直流信号,电压不小于 VREF+0.075V 时表示高电平,而电压不高于 VREF-0.075V 时表示低电平。VREF 本身应该介于 VDD 的 0.49 倍到 0.51 倍之间。
在连接 FPGA 的时候,有两种选择:
对于 7 Series 的 FPGA,Xilinx 要求如下:
For DDR3 SDRAM interfaces running at or below 800 Mb/s (400 MHz),
users have the option of selecting Internal VREF to save two I/O
pins or using external VREF. VREF is required for banks containing
DDR3 interface input pins (DQ/DQS).
进一步,Xilinx 在 UltraScale 文档下解释了背后的原因:
The UltraScale internal VREF circuit includes enhancements compared
to the 7 Series internal VREF circuit. Whereas 7 Series MIG had datarate
limitations on internal VREF usage (see (Xilinx Answer 42036)), internal
VREF is recommended in UltraScale. The VREF for 7 Series had coarse steps
of VREF value that were based on VCCAUX. This saved pins but limited the
performance because VCCAUX did not track with VCCO as voltage went up and
down. Not being able to track with VCCO enforced the performance
limitations of internal VREF in MIG 7 Series. UltraScale includes several
changes to internal VREF including a much finer resolution of VREF for DDR4
read VREF training. Additionally, internal VREF is based on the VCCO supply
enabling it to track with VCCO. Internal VREF is not subject to PCB and
Package inductance and capacitance. These changes in design now give internal
VREF the highest performance.
用中文简单来说:
以 MA703FA-35T 开发板为例,它使用的 FPGA 是 Artix7 35T,内存是 DDR3,采用的是 External VREF。它采用了 TPS51200 Sink and Source DDR Termination Regulator 芯片,将芯片的 REFOUT 芯片接到 DRAM 的 VREFDQ 和 VREFCA 引脚上。
本文的内容已经整合到知识库中。
DRAM 分成很多层次:Bank Group,Bank,Row,Column,从大到小,容量也是各级别的乘积。
举例子:
那么总大小就是 4*4*32768*1024*4=2 Gb
。
DRAM 的访问模式决定了访问内存的实际带宽。对于每次访问,需要这样的操作:
可以看到,如果访问连续的地址,就可以省下 ACT 命令的时间,可以连续的进行 RD/WR 命令操作。
除了显式 PRE 以外,还可以在某次读写之后自动进行 PRE:WRA(Write with Auto-Precharge) 和 RDA(Read with Auto-Precharge)。
总结一下上面提到的六种命令:
除此之外,还有一些常用命令:
DRAM 有很多参数,以服务器上的内存 MTA36ASF2G72PZ-2G3A3 为例子:
容量:每个 DRAM 颗粒 64K*1K*4*4*4=4Gb
,不考虑 ECC,一共有 16*2=32
个这样的颗粒,实际容量是 16 GB。32 个颗粒分为两组,每组 16 个颗粒,两组之间通过 CS_n 片选信号区分。每组 16 个颗粒,每个颗粒 4 位 DQ 数据信号,合并起来就是 64 位,如果考虑 ECC 就是 72 位。
再举一个 FPGA 开发板上内存的例子:MT40A512M16LY-075E,参数如下:
13.50ns(=18*0.750)
13.50ns(=18*0.750)
13.50ns(=18*0.750)
总大小:2*4*64K*1K*16=1GB
。这个开发板用了 5 个 DRAM 芯片,只采用了其中的 4.5 个芯片:最后一个芯片只用了 8 位数据,这样就是 4.5*16=72
位的数据线,对应 64 位+ECC。
可以看到,上面的 DRAM Datasheet 里提到了三个时序参数:
如果第一次访问一个 Row 中的数据,并且之前没有已经打开的 Row,那么要执行 ACT 和 RD 命令,需要的周期数是 RCD+CL;如果之前已经有打开了的 Row,那么要执行 PRE,ACT 和 RD 命令,需要的周期数是 RP+RCD+CL。但如果是连续访问,虽然还需要 CL 的延迟,但是可以流水线起来,充分利用 DDR 的带宽。
如果把这个换算到 CPU 角度的内存访问延迟的话,如果每次访问都是最坏情况,那么需要 17+17+17=51 个 DRAM 时钟周期,考虑 DRAM 时钟是 1200MHz,那就是 42.5ns,这个相当于是 DRAM 内部的延迟,实际上测得的是 100ns 左右。
更严格来说,读延迟 READ Latency = AL + CL + PL,其中 AL 和 PL 是可以配置的,CL 是固有的,所以简单可以认为 READ Latency = CL。同理 WRITE Latency = AL + CWL + PL,可以简单认为 WRITE Latency = CWL。CWL 也是可以配置的,不同的 DDR 速率对应不同的 CWL,范围从 1600 MT/s 的 CWL=9 到 3200 MT/s 的 CWL=20,具体见 JESD79-4B 标准的 Table 7 CWL (CAS Write Latency)。
用 Micron 提供的 Verilog Model 进行仿真,可以看到如下的波形图:
首先看第一个命令,ACT_n=0, ADDR=0x009C, CAS_n_A15=0, CKE=1->1, CS_n=0, RAS_n_A16=0, WE_n_A14=1,查阅标准可知这是 ACT(Bank Activate) 命令。接着第二个命令,ACT_n=1, ADDR=0x0400, CAS_n_A15=0, CKE=1->1, CS_n=0, RAS_n_A16=1, WE_n_A14=1, A10=1, 这是 RDA(Read with Auto Precharge) 命令。若干个周期后,读取的数据从 DQ 上输出,一共 8 个字节的数据。
DRAM 的一个特点是需要定期刷新。有一个参数 tREFI,表示刷新的时间周期,这个值通常是 7.8us,在温度大于 85 摄氏度时是 3.9 us(见 JESD79-4B Table 131)。在刷新之前,所有的 bank 都需要 Precharge 完成并等待 RP 的时间,这时候所有的 Bank 都是空闲的,再执行 REF(Refresh) 命令。等待 tRFC(Refresh Cycle) 时间后,可以继续正常使用。
为了更好的性能,DDR4 标准允许推迟一定次数的刷新,但是要在之后补充,保证平均下来依然满足每过 tREFI 时间至少一次刷新。
如果研究 DRAM 内存控制器,比如 FPGA 上的 MIG,可以发现它可以配置不同的地址映射方式,例如:
就是将地址的不同部分映射到 DRAM 的几个地址:Row,Column,Bank。可以想象,不同的地址映射方式针对不同的访存模式会有不同的性能。对于连续的内存访问,ROW_COLUMN_BANK 方式是比较适合的,因为连续的访问会分布到不同的 Bank 上,这样性能就会更好。
此外,如果访问会连续命中同一个 Page,那么直接读写即可;反之如果每次读写几乎都不会命中同一个 Page,那么可以设置 Auto Precharge,即读写以后自动 Precharge,减少了下一次访问前因为 Row 不同导致的 PRE 命令。一个思路是在对每个 Page 的最后一次访问采用 Auto Precharge。
DDR SDRAM 的传输速率计算方式如下:
Memory Speed (MT/s) * 64 (bits/transfer)
例如一个 DDR4-3200 的内存,带宽就是 3200 * 64 = 204.8 Gb/s = 25.6 GB/s
。但前面已经看到,除了传输数据,还需要进行很多命令,实际上很难达到 100% 的带宽。然后 CPU 可以连接多个 channel 的 DRAM,再考虑多个 CPU Socket,系统的总带宽就是
Memory Speed (MT/s) * 64 (bits/transfer) * Channels * Sockets
使用 MLC 等工具进行测试,计算实际与理论的比值,我测试得到的大概在 70%-90% 之间。
HBM 相比前面的 DDR SDRAM,它堆叠了多个 DRAM,提供多个 channel 并且提高了位宽。例如 Micron HBM with ECC,堆叠了 4/8 层 DRAM,提供 8 个 channel,每个 channel 的数据宽度是 128 位,以 3200 MT/s 计算,一个 HBM 芯片的传输速率最大是:
3200 (MT/s) * 128 (bits/transfer) * 8 (Channels) = 3276.8 Gb/s = 409.6 GB/s
所以一片 HBM 的传输速率就相当于 16 个传统的 DDR SDRAM:8 个 Channel 加双倍的位宽。128 位实际上就是把两片 64-bit DDR SDRAM 并起来了,可以当成一个 128 位的用,也可以在 Pseudo Channel 模式下,当成共享地址和命令信号的两个 DDR SDRAM 用。
Xilinx 的 Virtex Ultrascale Plus HBM FPGA 提供了 1800 (MT/s) * 128 (bits/transfer) * 8 (Channels) = 230.4 GB/s
的带宽,如果用了两片 HBM 就是 460.8 GB/s。暴露给 FPGA 逻辑的是 16 个 256 位的 AXI3 端口,AXI 频率 450 MHz,内存频率 900 MHz。可以看到,每个 AXI3 就对应了一个 HBM 的 pseudo channel。每个 pseudo channel 是 64 位,但是 AXI 端口是 256 位:在速率不变的情况下,从 450MHz 到 900MHz,再加上 DDR,相当于频率翻了四倍,所以位宽要从 64 位翻四倍到 256 位。
当然了,HBM 的高带宽的代价就是引脚数量很多。根据 HBM3 JESD238A,每个 Channel 要 120 个 pin,一共 16 个 channel(HBM2 是 8 channel,每个 channel 128 位;HBM3 是 16 channel,每个 channel 64 位),然后还有其他的 52 个 pin,这些加起来就 1972 个 pin 了。所以一般在 Silicon Interposer 上连接,而不是传统的在 PCB 上走线(图源 A 1.2V 20nm 307GB/s HBM DRAM with At-Speed Wafer-Level I/O Test Scheme and Adaptive Refresh Considering Temperature Distribution):
所以在 HBM3 标准里,用 Microbump 来描述 HBM 的 pin。
可以理解为把原来插在主板上的内存条,通过堆叠,变成一个 HBM Die,然后紧密地连接到 CPU 中。但是另一方面,密度上去了,价格也更贵了。
A100 显卡 40GB PCIe 版本提供了 1555 GB/s 的内存带宽。根据倍数关系,可以猜测是 5 个 8GB 的 HBM,每个提供 1555 / 5 = 311 GB/s
的带宽,那么时钟频率就是 311 (GB/s) * 8 (bits/byte) / 128 (bits/transfer) / 8 (channels) / 2 (DDR) = 1215 MHz
,这与 nvidia-smi -q
看到的结果是一致的。
进一步,A100 80GB PCIe 版本提供了 1935 GB/s 的带宽,按照同样的方法计算,可得时钟频率是 1935 (GB/s) / 5 * 8 (bits/byte) / 128 (bits/transfer) / 8 (channels) / 2(DDR) = 1512 MHz
,与 Product Brief 一致。频率的提高是因为从 HBM2 升级到了 HBM2e。
A100 文档中的 Memory bus width 5120 的计算方式也就清楚了:128 (bits/transfer) * 8 (channels) * 5 (stacks) = 5120 (bits)
。
H100 SXM5 升级到了 HBM3,内存容量依然是 80GB,但是时钟频率提高,内存带宽是 2619 (MHz) * 2 (DDR) * 128 (bits/transfer) * 8 (channels) * 5 (stacks) / 8 (bits/byte) = 3352 GB/s
。
之前用过一些 RISC-V 核心,但是遇到调试相关的内容的时候就两眼一抹黑,不知道原理,出了问题也不知道如何排查,趁此机会研究一下工作原理。
为了调试 RISC-V 核心,需要很多部件一起工作。按 RISC-V Debug Spec 所述,有这么几部分:
可以看到,DMI 是实际的调试接口,而 JTAG 可以认为是一个传输协议。
首先什么是 JTAG?简单来说,它工作流程是这样的:
具体来说,JTAG 定义了两类寄存器:IR 和 DR。可以把 JTAG 理解成一个小的总线,我通过 IR 选择总线上的设备,通过 DR 向指定的设备上进行数据传输。比如在 RISC-V Debug Spec 里面,规定了以下的 5 位 IR 地址定义:
可以类比为有四个设备:BYPASS,IDCODE,dtmcs,dmi,对应了一些地址。如果要选择 dtmcs 这个设备,就在 ShiftIR 阶段向 TDI 输入二进制的 00001 即可。选择地址以后,再向 DR 写入时,操作的就是 dtmcs 设备。
那么,每个设备是怎么操作的呢?假如我已经通过 IR 设置了当前设备是 dtmcs,然后进入 ShiftDR 模式时,JTAG 会同时输入和输出。输入的就是当前要输入的数据,输出的就是原来寄存器里的结果,这个结果可能是固定的,也可能是表示上一次输入对应的结果。
举个例子:IDCODE 设备,在 CaptureDR 阶段的时候,DR 总会被设为一个固定的 IDCODE,表示设备的 ID;在 Shift 的时候,这个 IDCODE 就会一位一位从 TDO 中输出,而 TDI 输入的数据都会被忽略掉。BYPASS 设备则是一个 1 位的寄存器,直接从 TDI 到寄存器,寄存器到 TDO,数据就这么流过去了。
那么,在 RISC-V Debug 里面,JTAG 是怎么用的呢?我们可以这么类比一下:CaptureDR 相当于读取寄存器到缓冲区,然后 ShiftDR 在读取缓冲区的同时写入缓冲区,最后 UpdateDR 则是把缓冲区中的数据写入到寄存器中。这和 MMIO 有点类似,只不过每次操作不是单独的写和读,而是一次操作等于先读后写。
还是来看例子。dtmcs 这个设备表示的是 DTM 当前的状态,它有 32 位,读取的时候可以得到 DMI 的状态和配置,写入的时候可以 reset DMI。以 OpenOCD 代码 dtmcontrol_scan
为例子,它做了这么几个事情:
如果我只想读取 dtmcs 寄存器,那么只要设置写入数据为 0 即可,因为寄存器的设计里考虑到,如果写入全 0 是没有副作用的。同理,如果只想写入 dtmcs 寄存器,直接写入即可,因为设计的时候也保证读入寄存器的值是没有副作用的。这样,就在一个一读一写的操作中,实现了读或者写的功能。
那么,dmi 寄存器的用途是什么呢?我们前面提到过,JTAG 其实是一个传输层,而 DMI 又定义了一系列的寄存器,这会让人有点混乱,为啥到处都是寄存器?又是 JTAG 的 IR/DR,又是 dmi,dmi 又有一堆寄存器,这是什么关系?
首先我们来看 dmi 寄存器的定义。它由三部分组成:地址、数据和操作。由于 JTAG 每次操作是一读一写,虽然寄存器定义差不多,但是读和写的含义是不同的。
比如读的时候,它表示的是上一次 dmi 请求的结果。地址还是上一次请求的地址,数据则是上一次请求的结果,操作字段 0 表示成功,2 表示失败,3 表示还没执行完。而写的时候,地址和数据表示了对哪个寄存器写入什么数据,操作字段 0 表示无操作,1 表示读,2 表示写。
可以看到,如果想操作 dmi 定义的寄存器,需要如下几个步骤,这也是 OpenOCD dmi_op_timeout
要做的事情:
可以预期,如果首先写入了一个写操作,那么第二次 DR scan 得到的结果就是是否成功写入;如果首先写入了一个读操作,那么第二次 DR scan 得到的结果就是目标寄存器的值。
可能看起来还是很绕,确实很绕,因为这里有一个封装的过程。首先,DMI 本身定义了一些寄存器,这些寄存器读/写都有一定的含义,比如控制某一个 RISC-V 核心暂停等等。接着,JTAG 需要传输 DMI 的读取和写入操作,同时还要考虑读写尚未完成的情况,怎么办?结论就是通过 DR 来实现,写入 DR 时,按照 DR 中的操作数,对应到 DMI 的写入/读取;然后读取 DR 的时候,按照 DMI 的状态,告诉 OpenOCD 目前是否已经完成了上一次 DMI 操作,和操作的结果。
讲完 JTAG 以后,终于来到了 DMI。其实 DMI 就是一系列的寄存器,类似于 MMIO 设备,只不过访问方式不是我们通常的内存读写,而是通过 JTAG 的方式进行。它有很多个寄存器,摘录如下:
OpenOCD 的 examine
函数对 DMI 初始化并进行一些参数的获取。它的操作如下:
类似地,其他各种调试操作都是对这些 DMI 寄存器的读和写进行。RISC-V Debug Spec 附录里还提到了如何实现调试器的一些功能。
比如要读取 CPU 的寄存器(比如通用寄存器,CSR 等等)的话,有如下的方式:
第一种是 Abstract Command,直接向 DMI 写入要寄存器编号,就可以实现读/写。
第二种是 Program Buffer。它是一块小的代码存储,可以通过 DMI 向其中写入指令,比如 csrw s0, mstatus; ebreak
,然后设置 s0 寄存器的值,再执行 Program Buffer 里的代码。
以 OpenOCD 代码为例,register_read_abstract
做了以下操作:
如果要读取内存的话,也有两种方法。一种是直接向 DMI 写入要读取的总线地址,然后再向指定的寄存器中读取数据。第二种还是利用 Program Buffer,写入一条 lw s0, 0(s0)
指令,然后先向 s0 写入地址,执行 Program Buffer 后,再把 s0 寄存器的值读出来。
那么,如何实现上面提到的 Abstract Command(比如读写寄存器,读写内存等)呢?Debug Spec 里面提到一种 Execution-Based 的方式,即在 Debug mode 下,核心依然在执行代码,只不过执行的是调试用的特殊代码。它做的就是轮询 Debug Module 等待命令,接受到命令以后,就去读写寄存器/内存,然后通过 data0-12 来传输数据。
这里还有一个比较特别的点,就是读取寄存器的时候,寄存器的编号是直接记录在指令中的,所以可以让 Debug Module 动态生成指令,然后让核心刷新 ICache 然后跳转过去。另外,还可以利用 dscratch0/dscratch1 寄存器来保存 gpr,然后用 dret 退出的时候再恢复,这样就有两个 gpr 可以用来实现功能了,实际上这已经够用了(一个技巧是,把地址设为 0 附近,然后直接用 zero 寄存器加偏移来寻址)。
在 dcsr 中,有一个值 step 表示是否在单步调试状态。设 step 为 1 的时候,如果不在 debug mode 中,只需要记录以及执行的指令数,当执行了一条指令后,视为下一个指令发生了进入 debug mode 的异常,这样就实现了单步调试。
调试器为了打断点,一种简单的方式是,往断点处写入 ebreak 指令,然后设置 dcsr 的 ebreakm/s/u,表示在这些特权集里,ebreak 是进入 debug mode,而不是原来的处理过程。然后,程序运行到 ebreak 指令的时候,进入 debug mode,openocd 发现核心进入 halted 状态后,让 gdb 继续进行调试。
硬件方面的实现方法就是,在遇到 ebreak 的时候,判断一下当前的特权集,结合 ebreakm/s/u 判断跳转到什么状态。此外,由于它会写入指令到内存,所以还需要执行 fence.i 指令,而 OpenOCD 需要依赖 progbuf 来执行 fence.i 指令,所以为了让这个方案工作,还得实现 Program Buffer。
当然了,软件断点也有局限性,比如内存不可写,比如 ROM,不能覆盖里面的指令,这样就有可能出问题。而且硬件断点性能也更好,不需要来回这样写指令。
ARM 有一种 semihosting 机制,就是处理器执行一种特定的指令序列,然后调试器看到整个序列的时候,不是进入 GDB 调试状态,而是去进行一些操作,比如输出信息,读写文件等等,然后结果通过 JTAG 写回去。OpenOCD 给 RISC-V 也做了类似的 semihosting 机制,只不过触发的指令序列不大一样,但是机制是类似的。
如果用过 Rocket Chip 仿真的或者以前的 ucb-bar/fpga-zynq 项目的话,会知道还有一个目的有些类似的东西:HTIF + fesvr,它是通过 fromhost/tohost 两组地址来进行通信,但是这个方法缺点是需要 poll tohost/fromhost 地址的内容,相对来说比较麻烦。
此外,debug spec 还有一个可选的功能,就是 Program Buffer,调试器可以往里面插入自定义的指令,然后结合 abstract command 进行操作。这样就可以做一些比较高效的操作,比如 OpenOCD 实现的批量写入内存:
并且设置 abstractauto,然后重复的操作是往 s1 里面写入新的数据,然后跳转到 program buffer,进行上面的 sw 操作,这样就可以一次 dmi 请求完成一次内存的写入,比较高效。
最近关于课程改革的讨论比较多,我也来谈谈我的看法。
一位高中毕业的同学,选择计算机系的动机是什么?我想了想,可能有下面几种:
学校希望计算机系培养出来的学生:
学校为希望计算机系:
计算机系为了实现上面的目标:
同时还要与其他院系(软件学院,交叉信息学院,集成电路学院等)有区分(分工)。
可以看到,上面的这一系列诉求是有矛盾的,可以假想这么几条路径:
这对于七字班(2017)或者更早同学来说,这可能是难以理解的。当时,保研不需要很高的 GPA,老师会看重科研潜力,想科研的同学可能选择在实验室科研的同时,放弃一些课程。
但是从八字班(2018)开始,多重因素下,问题就凸显了。一是总人数更多,保研难度本身就更大,竞争激烈;二是保研名额严格按照 GPA 排序,导致保研的同学必须科研学习两手抓;三是 GPA 改革以后,4.0 难度变低,以前会想 A B C 课程比较难,大部分人都拿不到 4.0,我 A 课程 3.7,B 课程 3.3 和你 A 课程 3.3,B 课程 3.7 是一样的,精力有限,只做三个里面最简单的一个,但现在会发现,比 GPA 实际上就是比谁 4.0 更多,虽然 A B C 课程也比较难,但是此时只能把三个都做了,不然就会排名下降明显。
这还会带来其他的问题:为了拿到更多 4.0,但精力有限,按照自己的时间,三个课里只能拿到一个 4.0,比不过拿到三个 4.0 的同学,咋办?抄袭。让老师放水,人人 4.0。
那么,怎么解决这个问题?
从个人的角度出发:尽早想好自己要什么。我要做什么,就在我要做的方向上做好,其他方向可以选择性放弃,不要随大流。
从院系的角度出发:难。
MIC: Many Integrated Core Architecture
Knights Corner:
4 路 SMT,AVX512 指令,32 KB L1I,32 KB L1D,每核心 512KB L2,乱序执行,一条 512 位计算流水线,每个周期双精度性能 512 / 64 * 2 = 16 FLOP/cycle
。61 核 1.053GHz 双精度性能是 16 * 61 * 1.053 = 1028 GFLOPS
。
向量寄存器分为四组,每组 128 位,两个 DP/四个 SP。SP 和 DP 计算共享乘法器,来优化面积。
Knights Landing:
核心:4 路 SMT,AVX512 指令,乱序执行,两条 512 位计算流水线,每个周期双精度性能 512 / 64 * 2 * 2 = 32 FLOP/cycle
,如果是 64 核 1.3 GHz,总双精度性能是 32 * 64 * 1.3 = 2662 GFLOPS
。一共 36 个 Tile,每个 Tile 有 2 Core + 2 VPU/core + 1MB 16-way L2,最大 72 个核心。
内存:6-channel 384GB DDR4 2400 RAM(理论 2400 * 6 * 8 = 115.2 GB/s
),8-16GB 3D MCDRAM(400+ GB/s)。
内存:4 组,每组 8GB HBM2,带宽 256 GB/s(1024 bit * 2G
),总共 32GB HBM2,带宽 1TB/s。Cache Line 大小 256 B。
核心:4 个 NUMA Node(Core Memory Group),每个 NUMA Node 包括 12 计算核,有 8MB 16 路的 L2 Cache。总共 48 计算核,4 辅助核。
指令集:ARMv8.2+SVE,512 位向量宽度,乱序执行,两个浮点流水线和两个整数流水线,每个周期双精度性能 512 / 64 * 2 * 2 = 32 FLOP/cycle
,主频 2.2 GHz,按主频算理论双精度浮点性能 32 * 2.2 * 48 = 3.4 TFLOPS
。文档里写的是双精度浮点性能 2.7 TFLOPS,单精度 5.4 TFLOPS,半精度 10.8 TFLOPS,8 位整数 21.6 TOPS,应该是按照实际测出来的算。TOP 500 配置是 7630848 核,对应 7630848 / 48 = 158976
个节点,Rpeak 是 537212 TFLOPS
,那么每个节点是 537212 / 158976 = 3.38 TFLOPS
,和上面的 3.4 接近。Linpack 跑出来的 Rmax 是 442010 TFLOPS,每个节点是 442010 / 158976 = 2.78 TFLOPS
,和文档里说的比较接近。
部分主要特性:
R0=R0+R1*R2
,A64FX 可以合并 R0=R3,R0=R0+R1*R2
两条为一条 R0=R3+R1*R2
指令型号 | 工艺 | Peak DP(TFLOPS) | 功耗 (W) | 性能功耗比 (TFLOPS/W) |
---|---|---|---|---|
P100 | 16 nm FinFET+ | 4.7 | 250 | 0.019 |
V100 | 12 nm FFN | 7 | 250-300 | 0.023-0.028 |
A100 | 7 nm N7 | 9.7 | 250-400 | 0.024-0.039 |
型号 | 内存容量 (GB) | 内存带宽 (GB/s) | 内存类型 | L2 缓存大小 | 寄存器堆大小 |
---|---|---|---|---|---|
P100 | 12-16 | 549-732 | 4096 bit HBM2 | 4096 KB | 14336 KB |
V100 | 16-32 | 900 | 4096 bit HBM2 | 6144 KB | 20480 KB |
A100 | 40-80 | 1555-2039 | 5120 bit HBM2 | 40960 KB | 27648 KB |
型号 | SM 数量 | CUDA 核心数 | FP64 核心数 | SM 频率 (MHz) |
---|---|---|---|---|
P100 | 56 | 3584 | 1792 | 1328 |
V100 | 80 | 5120 | 2560 | 1380 |
A100 | 108 | 6912 | 3456 | 1410 |
Sunway TaihuLight 的层次:
MPE 双精度性能:16 FLOP/cycle * 1.45 GHz = 23.2 GFlops
CPE 双精度性能:8 FLOP/cycle * 1.45 GHz = 11.6 GFlops
CPE 单精度性能:8 FLOP/cycle * 1.45 GHz = 11.6 GFlops
单节点双精度性能:4 * 8 * 8 * 11.6 + 4 * 23.2 = 3.0624 TFlops
Sunway TaihuLight 双精度性能:40 * 4 * 256 * 3.0624 = 125.435904 PFlops
MPE: 32KB L1I, 32 KB L1D, 256 KB L2(中文文献里写的是 512 KB)。乱序执行,4 译码,7 发射(5 整数 2 浮点)。指令预取,分支预测,寄存器重命名,预测执行。5 条整数流水线,2 条 256 位 SIMD 浮点流水线。
CPE:16KB L1I,无 DCache,有 64KB 可重构局部数据存储器(SPM scratch pad memory/LDM local data memory)。2 译码 2 发射,乱序执行,1 条 256 位 SIMD 流水线,1 条整数流水线。不同精度的 SIMD 宽度不同,单精度浮点运算 128 位(4 个单精度),双精度浮点运算 256 位(4 个双精度)。从 SPM 每个周期可以读取 32 字节的数据(正好一个 SIMD 寄存器)。
每个 core group 中还有一个 MC(Memory Controller),连接 8GB DDR3 memory,每个 MC 内存带宽 128 bit * 2133 MT/s = 34.128 GB/s
,单节点内存带宽 4 * 34.128 = 136.512 GB/s
。在 Stream Triad 测试,每个 core group 用 DMA 从内存到 SPM 传输数据带宽为 22.6 GB/s,而全局读写 gload/gstore 带宽只有 1.5 GB/s。访问全局内存需要 120+ 个周期。
8x8 矩阵中的从核可以在同行和同列方向上进行低延迟和高带宽的数据传递:2 个从核点对点通信延迟不超过 11 个周期,单个 core group 寄存器通信集合带宽达到 637 GB/s。
28nm 工艺流片,芯片 die 面积超过 500 mm^2,峰值功耗 292.7W,峰值能效比达 10.559 GFLOPS∕W(HPL 6.05 GFLOPS/W)。
SW26010-Pro 是升级版 SW26010,升级的内容在于:
6 * (8 * 8 + 1) = 390
个核心。频率也提高了,MPE 频率 2.1GHz,CPE 频率 2.25 GHz。SIMD 宽度扩展到 512 位。128 bit * 3200 MT/s = 51.2 GB/s
;单节点总内存 96 GB,总内存带宽 51.2 * 6 = 307.2 GB/s
。在新闻稿和 Sunway supercomputer architecture towards exascale computing: analysis and practice 文章中出现,没有在今年发出来的论文里实际采用,名称可能是新闻稿自己编的,我猜可能没有实际采用,而是做了 SW26010P。和 SW26010 区别:
8 * (8 * 8 + 1) = 520
个核心。最近需要在 VCU128 上搭建一个 SOC,然后想到可以把 OpenSBI、U-Boot 和 Linux 移植到这个平台上方便测试,于是又开始折腾这些东西。代码仓库都已经开源:
第一部分是基于之前 rocket2thinpad 在 Thinpad 上移植 Rocket Chip 的经验,做了一些更新,主要是因为 VCU128 的外设不大一样,同时我也要运行更复杂的程序,主要做了这些事情:
主要踩过的坑:
在 U-Boot 上花了比较多的时间,用它的目的主要是:
移植的时候,首先新建一个自定义的 board,然后自己写 defconfig 和 dts,其中 dts 可以参考 rocket chip 生成的 dts 文件。然后,按照各个外设的 device tree binding 去写,然后打开/关闭各个 CONFIG 开关。
对代码主要的改动是,实现了 DCache 的 flush 功能,因为以太网部分用了 DMA,所以要让外设看到内存的更改,这里采用的是 SiFive 的扩展指令 cflush.d.l1
。由于编译器还不支持这个指令,就按照网上的方式去构造了汇编指令。实现完成以后,就可以用网络了。
一开始的时候,为了简单,直接在 M-mode 中运行 U-Boot,这样不需要 OpenSBI,同时 DTB 也是内置的。但后续为了运行 Linux,还是需要一个 SBI 实现:OpenSBI,然后在 S-mode 中运行 U-Boot,再引导到 Linux。
此外还花了很多努力来缩小 binary 大小,首先可以用 nm --size -r u-boot | head -20
来找到比较大的一些符号,不考虑其中 BSS 的部分(type=b),主要看哪些代码/数据比较占空间。
UPDATE: U-Boot 在 v2022.01 版本修复了一个 BUG,之前的版本在 riscv 架构下没有 reserve lmb region,使得加载 initrd 的时候,会覆盖掉自己的栈空间,这解释了之前的诸多玄学内存问题,升级到 v2022.01 后就好了。
OpenSBI 移植比较简单,直接参考 template 修改即可,主要就是串口的配置,其他基本不用改。然后,我把 U-Boot 作为 OpenSBI 的 Payload 放到 OpenSBI 的后面,此时要把 U-Boot 配置为 S-mode 模式。接着,遇到了新的问题:cflush.d.l1
指令只能在 M-mode 用,因此我在 OpenSBI 代码中处理了 trap,转而在 M-mode 里面运行这条指令。这样,就可以在 S-mode 里刷新 Cache 了。
Linux 目前可以 boot 到寻找 init,还没有碰文件系统,之后计划用 buildroot 打一个 initramfs 出来。为了在 U-Boot 中启动 Linux,用 U-Boot 的 mkimage 工具生成了 FIT 格式的 uImage,里面打包了 kernel image 和 dtb,就可以用 bootm 命令启动了,注意地址不要和加载地址重复。
此外还遇到一个坑:RV64 里面 Linux dts 的 address cell 得是 2(对应 64 位),否则会有错误。但 U-Boot 对这个没有做要求。
一开始的时候,AXI DMA 直接接到内存上,所以与 CPU 缓存是不一致的,网卡驱动需要经常地刷缓存。在 Rocket Chip 上,可以用 sifive 自己的 cflush 指令来刷缓存,但是它只能在 M 态执行,同时又支持虚拟地址,这种奇怪的设计就使得要在 OpenSBI,U-Boot 和 Linux 三处都添加逻辑:OpenSBI 处理 illegal instruction,如果发现是 cflush 指令,就再次 cflush;U-Boot 和 Linux 修改驱动,在合适的地方添加 cflush 指令。U-Boot 驱动比较简单,工作得比较好,但是 Linux 的网卡驱动怎么都改不好。
最后决定,打开 Rocket Chip 的 Frontend Bus,添加一个 AXI Slave 接口,然后让 AXI DMA 通过 AXI Slave 接入到 Rocket Chip 中,然后通过 TLBroadcast 实现缓存一致性。这样软件实现会比较简单,但是硬件就更复杂了。
$ esxcli software vib install -d $PWD/Net-Community-Driver_1.2.0.0-1vmw.700.1.0.15843807_18028830.zip
/vmfs/volumes/
里找到更新文件esxcli software sources profile list -d <zip>
esxcli software profile update -p <profile> -d <zip>
ref: Upgrade or Update a Host with Image Profiles
如果 CPU 比较旧,可能会有警告:Updated Plan for CPU Support Discontinuation In Future Major vSphere Releases,按照信息添加参数忽略即可,ESXi 7.0 系列都是支持的,如果之后出了新的版本可能不支持。
$ esxcli network firewall ruleset set -e true -r httpClient
# find profile name
$ esxcli software sources profile list -d https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml
# upgrade to 7.0u3 for example
$ esxcli software profile update -p ESXi-7.0U3-18644231-standard -d https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml
ref: Update Standalone ESXi Host
目前 OEM 版本还没找到在线升级方法,需要下载 zip 然后按照离线升级方法安装。
列出所有防火墙规则:
允许出站 SSH:
关闭出站 SSH:
参考:Solution: ESXi Installation with USB NIC only fails at 81%
发现以下博客有很多关于 ESXi 的内容:
参考 ESXi/vCSA Release Notes:
/storage/log
满,原因是访问 vmware 网站失败打印的日志太大:/storage/log/vmware/analytics/analytics-runtime.log*
;解决方法:vmon-cli -r analytics
重启服务,然后删掉旧的日志。/storage/log
满,原因是 tomcat 日志太大。XXX Service Health Alarm
:尝试重启对应服务,比如 vmon-cli -r perfcharts
对应 Performance Charts
,vmon-cli -r vapi-endpoint
对应 VMWare vAPI Endpoint
查看更新状态:cat /storage/core/software-update/stage_operation
;更新文件下载路径:/storage/updatemgr/software-update*/stage
。有一个包特别大:wcpovf
需要两个多 G。
CLI 更新方法:https://earlruby.org/2021/01/upgrading-vcenter-7-via-the-command-line/
首先,unregister 原来的 VM,然后把文件移动到新的路径下。对于 Thin Provisioned Disk,需要特殊处理,否则直接复制的话,会变成 Thick Provisioned Disk,正确方法是采用 vmkfstool
:
需要注意的是,这里的路径用的是不带 -flat
的 vmdk,因为这个文件记录了 metadata,而 -flat.vmdk
保存了实际的数据。可以用 du
命令看实际的硬盘占用,从而确认它确实是 Thin Provisioned。
如果已经在 Web UI 上复制了,你会发现无法停止复制,解决办法是:
这样就会重启 Web UI,不过等它恢复需要很长的时间,还要删掉 cookie。
本文的内容已经整合到知识库中。
之前一直没搞懂 Vivado 中 xdc 需要怎么编写,遇到一些必须要写 xdc 的时候就很头疼,不知道怎么写才可以得到正确的结果。今天分析了一下 AXI Quad SPI 的时序 xdc,终于理解了其中的含义。
AXI Quad SPI 是一个 SPI 的控制器,它支持 XIP(eXecute In Place)模式,即可以暴露一个只读 AXI Slave 接口,当接收到读请求的时候,就按照标准的 SPI Flash 命令去对应的地址进行读取,然后返回结果。由于不同厂家的 SPI Flash 支持有所不同,所以 IP 上的设置可以看到厂家的选择。
特别地,一个常见的需求是希望访问 Cfg(Configuration)Flash,亦即用来保存 Bitstream 的 Flash。当 FPGA 上电的时候,如果启动模式设置为 SPI Flash,FPGA 就会向 Cfg Flash 读取 Bitstream,Cfg Flash 需要连接到 FPGA 的指定引脚上,当 FPGA 初始化的时候由内部逻辑驱动,初始化完成后又要转交给用户逻辑。转交的方式就是通过 STARTUP 系列的 primitive。
通常,如果要连接外部的 SPI Flash,需要连接几条信号线到顶层,然后通过 xdc 把信号绑定到引脚上,然后引脚连接了一个外部的 SPI Flash。但由于 Cfg Flash 比较特殊,所以信号从 AXI Quad SPI 直接连到 STARTUP 系列的 primitive 上。如果是采用 STARTUPE2 原语的 7 系列的 FPGA,那么只有时钟会通过 STARTUPE2 pritimive 连接到 SPI Flash 上,其他数据信号还是正常通过顶层绑定;如果是采用 STARTUPE3 原语的 UltraScale 系列的 FPGA,那么时钟和数据都通过 STARTUPE3 primitive 连接到 SPI Flash。
把信号连好了只是第一步,因为外设对时序要求比较复杂,如果用一个比较高直接跑,很大可能就读取到错误的数据了。很贴心的是,AXI Quad SPI 已经在生成的文件里提供了一个样例的 xdc,在文档里也有体现。在这里,我使用的设备是 Virtex Ultrascale+ 的 FPGA,其他系列的 FPGA 会有所不一样。它内容如下:
#### All the delay numbers have to be provided by the user
#### Following are the SPI device parameters
#### Max Tco
set tco_max 7
#### Min Tco
set tco_min 1
#### Setup time requirement
set tsu 2
#### Hold time requirement
set th 3
#####################################################################################################
# STARTUPE3 primitive included inside IP for US+ #
#####################################################################################################
set tdata_trace_delay_max 0.25
set tdata_trace_delay_min 0.25
set tclk_trace_delay_max 0.2
set tclk_trace_delay_min 0.2
create_generated_clock -name clk_sck -source [get_pins -hierarchical *axi_quad_spi_0/ext_spi_clk] [get_pins -hierarchical */CCLK] -edges {3 5 7}
set_input_delay -clock clk_sck -max [expr $tco_max + $tdata_trace_delay_max + $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
set_input_delay -clock clk_sck -min [expr $tco_min + $tdata_trace_delay_min + $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
set_multicycle_path 2 -setup -from clk_sck -to [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]]
set_multicycle_path 1 -hold -end -from clk_sck -to [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]]
set_output_delay -clock clk_sck -max [expr $tsu + $tdata_trace_delay_max - $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];
set_output_delay -clock clk_sck -min [expr $tdata_trace_delay_min - $th - $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];
set_multicycle_path 2 -setup -start -from [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]] -to clk_sck
set_multicycle_path 1 -hold -from [get_clocks -of_objects [get_pins -hierarchical */ext_spi_clk]] -to clk_sck
我们分段来看这个 xdc 都做了什么:
create_generated_clock -name clk_sck -source [get_pins -hierarchical *axi_quad_spi_0/ext_spi_clk] [get_pins -hierarchical */CCLK] -edges {3 5 7}
首先,它创建了一个时钟 clk_sck
。CCLK 是 STARTUP 输出的实际时钟,会连接到 Cfg Flash 的时钟信号上。而 AXI Quad SPI 的 ext_spi_clk 会输出到 CCLK 上,因此这里是一个生成的时钟,并且指定上下边沿的位置。edges
参数有三个,分别表示上升、下降和上升沿分别的位置。1 表示源时钟的第一个上升沿,2 表示源时钟的第一个下降沿,以此类推,所以 {3, 5, 7} 的意思就是频率减半,相位差半个周期。
接着,最主要的就是,怎么设置延迟。可以看到,代码中首先定义了一些参数:
#### Max Tco
set tco_max 7
#### Min Tco
set tco_min 1
#### Setup time requirement
set tsu 2
#### Hold time requirement
set th 3
#### Trace delay
set tdata_trace_delay_max 0.25
set tdata_trace_delay_min 0.25
set tclk_trace_delay_max 0.2
set tclk_trace_delay_min 0.2
首先是 \(t_{co}\),应该表示的是 SPI Flash 的时钟到输出的延迟。本文用的 SPI Flash 型号是 Micron MT25QU02GCBB8E12-0SIT,可以从它的 Datasheet 看到,时钟到输出的延迟应该是 Max 7ns:
因此 tco_max
设为 7,tco_min
默认即可,因为 Datasheet 中没有做要求。
然后 \(t_{su}\) 和 \(t_h\) 则是输入的 setup 和 hold time。类似的,可以查到 SPI Flash 的参数:
所以 tsu
设为 2.5,th
设为 2。
接下来则是 tdata 和 tclk 的 trace delay。这指的是从 FPGA 引脚到 SPI Flash 引脚的信号传输延迟。从严谨的角度来说,可以从板子的布线上测量长度来计算出来,不过这里就先用默认值了。一个简单的估算方法:光速 \(3*10^8 \text{m/s}\),考虑电信号传播速度是光速的一半,可以得到延迟和长度的比值: \(0.06 \text{ns/cm} = 0.15 \text{ns/inch}\)。
那么,这些变量怎么参与到 input/output delay 的计算呢?
首先考虑 input delay。它指的是,从 SPI Flash 到 FPGA 的数据,相对于时钟的延迟。这个延迟由三部分组成:
a -> b
b -> c
c -> d
因此总延迟就是 \(t_{clk}+t_{co}+t_{data}\),就可以得到对应的设置:
set_input_delay -clock clk_sck -max [expr $tco_max + $tdata_trace_delay_max + $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
set_input_delay -clock clk_sck -min [expr $tco_min + $tdata_trace_delay_min + $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_IN[*]] -clock_fall;
接下来要考虑 output delay。虽然 output delay 也有 min 和 max,但其含义有所区别,需要分别考虑。
首先是 max,它对应的是 setup time。如果定义时间 0 为时钟的上升沿,沿更早的时间为正的时间轴,沿更晚的时间为负的时间轴。那么,我们希望的是,数据到达寄存器输入的时间大于 setup time,此时可以满足 setup 条件。那么,具体怎么算呢?注意,我们要考虑的是从 FPGA 数据输出到 SPI Flash 上时钟的延迟。
假设 FPGA CCLK 时钟上升沿在 \(0\) 时刻(下图的 a
),那么 SPI Flash 时钟上升沿在 \(-t_{clk}\) 时刻(下图的 b
)。假设 FPGA 数据输出时刻为 \(t_0\)(通常为正,下图的 c
),那么 FPGA 数据输出到达 SPI Flash 在 \(t_0-t_{data}\) 时刻(下图的 d
),我们期望 \(t_0-t_{data}\) 在 \(-t_{clk}\) 时刻之前(下图的 d -> b
)至少 \(t_{su}\) 时间到达,可以得到表达式:
化简一下,就可以得到 \(t_0 > t_{data} + t_{su} - t_{clk}\),如果考虑极端情况,右侧 \(t_{data}\) 取最大值,\(t_{clk}\) 取最小值,我们就可以得到约束:
set_output_delay -clock clk_sck -max [expr $tsu + $tdata_trace_delay_max - $tclk_trace_delay_min] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];
接下来考虑 output delay 的 min,这对应的是 hold time。我们希望数据到达 SPI Flash 寄存器的时候,距离上升沿时间超过了 \(t_h\)。还是一样的假设,如果 FPGA CCLK 时钟上升沿在 0 时刻(下图的 a
),那么 SPI Flash 时钟上升沿在 \(-t_{clk}\) 时刻(下图的 b
)。假设 FPGA 数据输出时刻为 \(t_0\)(下图的 c
),那么 FPGA 数据输出到达 SPI Flash 在 \(t_0-t_{data}\) 时刻(下图的 d
),要求满足 hold 条件,可以得到:
化简以后,可以得到 \(t_0 < t_{data} - t_{clk} - t_h\),按照极限来取,\(t_{data}\) 取最小值,\(t_{clk}\) 取最大值,可以得到最终的时序约束:
set_output_delay -clock clk_sck -min [expr $tdata_trace_delay_min - $th - $tclk_trace_delay_max] [get_pins -hierarchical *STARTUP*/DATA_OUT[*]];
这样就可以实现 FPGA 和 SPI Flash 之间的正常通讯了。我觉得,这里比较绕的就是时间轴的定义,和我们平常思考的是反过来的。而且,这里的 min 和 max 并不是指 \([\min, \max]\),而是 \((-\inf, \min] \cup [\max, \inf)\)。代入上面的数据,可以得到 \(\max=2.05, \min=-2.95, t_0 \in (\inf, -2.95] \cup [2.05, \inf)\)。如果变化的时刻距离时钟上升沿太接近,就会导致在 SPI Flash 侧出现不满足 setup 或者 hold 约束的情况。
也可以换个角度来理解 min 和 max:对于同一个周期的时钟和数据来说,数据相对时钟有一个延迟,这个延迟不能太小,至少要满足 hold,所以这是一个最小的延迟;同时这个延迟不能太大,最多需要满足下一个时钟上升沿的 setup,所以这是一个最大的延迟。如果从这个角度来看,那就是延迟在一个 \([\min, \max]\) 的范围内。但是,这样在计算的时候就需要把时钟周期纳入到 \(\max\) 的计算中,比如 \(\max=t_c-t_{su}\)。如果我们把坐标轴修改一下,原点变成原来的下一个时钟周期的上升沿,x 的正方向变成反向,就可以得到上面的形式了。
那么,更常见的 FPGA 是 7 系列的,比如 Artix 7,它采用的是 STARTUPE2 原语,只有时钟是通过 STARTUPE2 原语的 USRCCLKO 信号传递到 CCLK 引脚上的,其他数据信号都是需要在顶层信号绑定对应的引脚。在 AXI Quad SPI 文档中,描述了 STARTUPE2 所需要的时序约束,我们分段来分析一下。
# You must provide all the delay numbers
# CCLK delay is 0.5, 6.7 ns min/max for K7-2; refer Data sheet
# Consider the max delay for worst case analysis
set cclk_delay 6.7
# Following are the SPI device parameters
# Max Tco
set tco_max 7
# Min Tco
set tco_min 1
# Setup time requirement
set tsu 2
# Hold time requirement
set th 3
# Following are the board/trace delay numbers
# Assumption is that all Data lines are matched
set tdata_trace_delay_max 0.25
set tdata_trace_delay_min 0.25
set tclk_trace_delay_max 0.2
set tclk_trace_delay_min 0.2
### End of user provided delay numbers
可以看到,这一部分和上面 UltraScale+ 部分差不多,只是多一个 cclk_delay
变量,这是因为 Artix 7 中,时钟只能创建到 USRCCLKO 引脚上,但是实际 SPI Flash 接收到的时钟等于 USRCCLKO 到 CCLK 引脚,然后再通过 PCB 上的线传播到 SPI Flash,所以需要手动添加一个偏移,这个偏移就是 USRCCLKO 到 CCLK 的延迟,可以在 Artix 7 Data Sheet 里面看到:对于 1.0V,-2 速度的 FPGA,这个延迟最小值为 0.50ns,最大值为 6.70ns,这里采用了最大值。
所以,下面的约束,除了时钟部分以外,和上面分析的 UltraScale+ 时序约束计算方法是相同的。不同点在于,首先约束了从 AXI Quad SPI 到 STARTUPE2 的路由时延,从 0.1ns 到 1.5ns,然后又从 USRCCLKO 创建了一个分频 + 延迟 cclk_delay
纳秒的时钟,作为 SPI Flash 上 SCK 引脚的时钟。
# this is to ensure min routing delay from SCK generation to STARTUP input
# User should change this value based on the results
# having more delay on this net reduces the Fmax
set_max_delay 1.5 -from [get_pins -hier *SCK_O_reg_reg/C] -to [get_pins -hier
*USRCCLKO] -datapath_only
set_min_delay 0.1 -from [get_pins -hier *SCK_O_reg_reg/C] -to [get_pins -hier
*USRCCLKO]
# Following command creates a divide by 2 clock
# It also takes into account the delay added by STARTUP block to route the CCLK
create_generated_clock -name clk_sck -source [get_pins -hierarchical
*axi_quad_spi_1/ext_spi_clk] [get_pins -hierarchical *USRCCLKO] -edges {3 5 7}
-edge_shift [list $cclk_delay $cclk_delay $cclk_delay]
# Data is captured into FPGA on the second rising edge of ext_spi_clk after the SCK
falling edge
# Data is driven by the FPGA on every alternate rising_edge of ext_spi_clk
set_input_delay -clock clk_sck -max [expr $tco_max + $tdata_trace_delay_max +
$tclk_trace_delay_max] [get_ports IO*_IO] -clock_fall;
set_input_delay -clock clk_sck -min [expr $tco_min + $tdata_trace_delay_min +
$tclk_trace_delay_min] [get_ports IO*_IO] -clock_fall;
set_multicycle_path 2 -setup -from clk_sck -to [get_clocks -of_objects [get_pins
-hierarchical */ext_spi_clk]]
set_multicycle_path 1 -hold -end -from clk_sck -to [get_clocks -of_objects [get_pins
-hierarchical */ext_spi_clk]]
# Data is captured into SPI on the following rising edge of SCK
# Data is driven by the IP on alternate rising_edge of the ext_spi_clk
set_output_delay -clock clk_sck -max [expr $tsu + $tdata_trace_delay_max -
$tclk_trace_delay_min] [get_ports IO*_IO];
set_output_delay -clock clk_sck -min [expr $tdata_trace_delay_min - $th -
$tclk_trace_delay_max] [get_ports IO*_IO];
set_multicycle_path 2 -setup -start -from [get_clocks -of_objects [get_pins
-hierarchical */ext_spi_clk]] -to clk_sck
set_multicycle_path 1 -hold -from [get_clocks -of_objects [get_pins -hierarchical */
ext_spi_clk]] -to clk_sck
一个 Artix 7 上配置 STARTUP SPI Flash 的例子 io_timings.xdc 可供参考。
用 k8s 也有一段时间了,之前遇到过 iptables 等出现问题,导致 k8s 节点间网络出现问题,于是想研究一下 k8s 的网络工作原理。
首先研究一下 Docker 网络连接是如何实现的。Docker 首先会创建一个 bridge,名为 bridge0:
$ ip a show docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:c4:87:73:bf brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:c4ff:fe87:73bf/64 scope link
valid_lft forever preferred_lft forever
默认情况下,每个容器都会有单独的一个 netns,然后创建一对 veth pair,一端留在 global netns,另一端放到容器中。在 global netns 中的 veth 端口会加入到 docker0 中:
$ ip a show dev veth3db9316
21: veth3db9316@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether e2:49:a6:2d:5a:bd brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::e049:a6ff:fe2d:5abd/64 scope link
valid_lft forever preferred_lft forever
$ brctl show docker0
bridge name bridge id STP enabled interfaces
docker0 8000.0242c48773bf no veth3db9316
容器中的网络,在 veth 上 docker 会分配并配置一个地址(比如 172.17.0.2),然后设置默认路由 via 172.17.0.1。一方面,可以通过默认路由到 172.17.0.1 再通过 iptables NAT 访问外面的网络:
$ iptables-save -t nat
# Generated by xtables-save v1.8.2 on Sat Sep 18 10:44:49 2021
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Sat Sep 18 10:44:49 2021
另一方面,因为连接不同容器的 veth 在同一个 bridge 下面,所以不同容器的可以认为在同一个二层网络中,自然可以互相访问。
在 k8s 中,所有的 pod 都希望可以通过 IP 地址互联。一个思路是把各个节点上的 pod 通过类似 docker 的方法实现,即每个 netns 通过 veth 连接到一个 bridge 上,然后再想办法去路由在其它节点上的 pod。
因为我用 k3s 搭建 k8s 集群,它用的 cni 是 flannel。flannel 采用的是 vxlan 的方式来实现节点间的网络通信。
首先还是看看节点内的 pod 如何组网。
5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
link/ether 6a:4f:ff:8b:b1:b3 brd ff:ff:ff:ff:ff:ff
inet 10.42.0.1/24 brd 10.42.0.255 scope global cni0
valid_lft forever preferred_lft forever
inet6 fe80::7cf6:57ff:fed7:c49b/64 scope link
valid_lft forever preferred_lft forever
6: vethc47d6140@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default
link/ether da:19:f8:48:f6:49 brd ff:ff:ff:ff:ff:ff link-netns cni-9d2a5120-16a3-453e-bf64-c4006c06c93b
inet6 fe80::d819:f8ff:fe48:f649/64 scope link
valid_lft forever preferred_lft forever
首先,flannel 给每个节点分配了一个 /24 的网段,比如第一个节点是 10.42.0.0/24,第二个是 10.42.1.0/24,依次类推。然后,节点内的 pod 就从这个网段里分配地址,比如 10.42.0.50/24,它的默认网关是 10.42.0.1。这些 veth 都会加入到 cni0 的 bridge 中。这一部分原理和 docker 是一样的,只不过名字不同了。也有相应的 iptables 规则:
$ iptables-save | grep MASQUERADE
-A POSTROUTING -s 10.42.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE --random-fully
-A POSTROUTING ! -s 10.42.0.0/16 -d 10.42.0.0/16 -j MASQUERADE --random-fully
那么,节点间网络如何实现呢?假如,我们要从第一个节点 pod 10.42.0.50/24 访问第二个节点的 pod 10.42.1.51/24,首先,pod 根据默认路由会发给 10.42.0.1/24,到达第一个节点的 cni0,然后查路由表:
$ ip r
10.42.0.0/24 dev cni0 proto kernel scope link src 10.42.0.1
10.42.1.0/24 via 10.42.1.0 dev flannel.1 onlink
可以看到,它会匹配 10.42.1.0/24 via 10.42.1.0 dev flannel.1 的路由。flannel.1 是一个 vxlan 的 interface:
$ ip a show flannel.1
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
link/ether b6:2f:39:4a:02:c0 brd ff:ff:ff:ff:ff:ff
inet 10.42.0.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
inet6 fe80::b42f:39ff:fe4a:2c0/64 scope link
valid_lft forever preferred_lft forever
当这个 interface 接收到一个 packet 的时候,会查询 fdb:
这个 fdb 中包括了 (MAC 地址,IP 地址) 的 tuple。当 flannel.1 收到一个 Ethernet Frame 的时候,如果目的地址匹配这里的 MAC 地址,就会直接把 Eth Frame 封装到 UDP 里面发给目的 IP 地址;否则,就会在这个表里面 broadcast。这样,第二个节点就会收到 packet 并且转给实际的 pod。
总结一下 k8s 的网络互联的实现方法:节点内通过 bridge 实现,把链接各个 netns 的 veth 桥接起来;节点间划分为多个子网,子网间通过 flannel 的网关进行路由,flannel 网关间通过 vxlan 进行互联。
本文的内容已经整合到知识库中。
最早学习乱序执行 CPU 的时候,是在 Wikipedia 上自学的,后来在计算机系统结构课上又学了一遍,但发现学的和现在实际采用的乱序执行 CPU 又有很大区别,后来又仔细研究了一下,觉得理解更多了,就想总结一下。
本文主要讨论乱序执行的部分。
本系列的所有文章:
最近在答疑的时候,发现同一条命令在不同系统上行为不同,一开始以为是 bash 版本问题,排查后最后发现是 locale 的问题。一个例子如下:
$ cat poc.txt | tr '\\n' ' '
1 + - * / \ a b A B 一 二 测 试 α
$ LANG="" sort poc.txt | tr '\\n' ' '
* + - / 1 A B \ a b α 一 二 测 试
$ LANG="zh_CN.UTF-8" sort poc.txt | tr '\\n' ' '
* + - / \ 1 测 二 试 一 a A b B α
$ LANG="en_US.UTF-8" sort poc.txt | tr '\\n' ' '
* + - / \ 1 a A b B α 一 二 测 试
注意 1 a A 的顺序,在不同的 locale 下结果不同。
网上也有关于这个问题的讨论:
有一台 RHEL 6 的服务器,各种软件版本太老了,用起来很难受,因此想升级。一开始想升级到 RHEL 7,但是发现必须要从 RedHat 下载 ISO,比较慢,所以我就先切换到 CentOS 6,再升级到 CentOS 7
一开始还是打算升级到 RHEL 7,所以跟随 RedHat 的文档去做 pre upgrade check,发现有一步要跑好久,网上搜了一下,发现这个步骤会扫描已有的各种程序,检查升级以后会不会出现不能运行的问题。但是如果有很多小文件,这一个过程就会进行很久,好在可以设置 exclusion 目录。最后检查出来的结果就是 GNOME 没法升级,建议卸载。
倒腾了一下升级工具,发现需要离线安装,比较麻烦,我就干脆换 CentOS 了。
首先,把软件源都切换到 CentOS,这一步很简单,因为包都是一样的。只不过,因为 CentOS 6 在 centos-vault 里面,所以用起来比较麻烦。
由于 CentOS 6 到 CentOS 7 升级涉及的改动比较多,官方提供了一个升级工具。一开始,我想直接升级到 CentOS 7 最新版本,但是报错,看到网上说可以升级到 CentOS 7 的早期版本,试了一下,确实没问题。
一通升级以后,重启,进入更新过程,发现很多包都安装失败了。重启以后,因为找不到 rootfs,挂在了 dracut 的 initramfs 里面。
简单试了一下,发现 dracut 的 initramfs 里程序太少了,调试起来很痛苦。所以,我在 BMC 里通过 Virtual Media 挂了一个 Arch Linux 的 Live CD。因为通过 Web 访问延迟太高,我设了一个 root 密码,然后直接 ssh 到 live cd 系统中。
接着,我发现,可以正常找到盘和里面的各个分区,所以怀疑是之前 initramfs 里缺了什么东西,导致找不到硬盘。我 arch-chroot 到 root 分区里,然后手动更新各个包,特别麻烦:我首先升级了 yum repos 到最新的 CentOS 7,然后手动删掉/升级 el6 的各个软件包。最后好不容易把 kernel 终于升级好了,又重新生成 grub2 的配置,因为 CentOS 6 是 grub1。这时候,重启进入系统,发现可以找到 rootfs 了,但是经过 selinux relabel 以后,仍然会遇到 systemd-logind 起不来的问题,伴随着一系列的 audit 报警。
最后,使出了暴力的解决办法:在 cmdline 中设置 selinux=0 audit=0,然后终于进入系统了。再继续删掉一些 el6 的包,然后升级各种包,最后终于是恢复了正常。
7.31 号周六的时候,发现 Intel Intrinsics Guide(https://software.intel.com/sites/landingpage/IntrinsicsGuide/) 出现错误,加载数据失败,于是在 Intel 的网站上提交了一个 bug。
8.2 号的时候,Intel 发邮件过来,说已经复现了问题,已经汇报给了后端团队。邮件原文:
Thank you for bringing this to our attention. We have verified and
encountered the same issue. Please know that we have escalated this
issue to our backend technical team.
We will get back to you as soon as we have an update. Have a nice day
ahead!
8.4 号的时候,Intel 再次发邮件过来,说后端团队正在处理这个问题,会尽快完成修复,请我耐心等待。这个时候我去网站上看,还是有问题。邮件原文:
Our backend team is still working on this issue. We are trying our level
best to get back to you with an update soon.
Have a nice day ahead!
8.6 号 19:27 的时候,Intel 又发了一次邮件,说后端团队依然在处理这个问题,并且正在进行一个永久性的修复(言下之意是现在提供了一个临时性的修复)。这个时候去网站上看,终于是修好了。邮件原文:
We have received an update from our backend team is that they are
working on this issue and, a more permanent fix is in the works.
Hopefully, it will resolve soon.
We appreciate your patience and understanding on this matter. Have a
nice day!
我回复了一下邮件,告诉 Intel 我这边看到已经是修复好的版本,紧接着又收到了一封邮件,告诉我可以从网站上下载离线版的 Intrinsics Guide:
Thank you for your prompt response. We are glad that your issue has been
resolved and we would like to thank you for your co operation. Please
be informed that the offline version of the Intrinsic Guide is now
available for download from the site. The offline version of the guide
has the same content as the site, but is viewable offline by the user. A
link to the download is now added in the left column of the site:
https://software.intel.com/sites/landingpage/IntrinsicsGuide/
That said, we are closing this ticket and if you have further issues
please open another ticket and we will be happy to help you.
After case closure, you will receive a survey email. We appreciate it if
you can complete this survey regarding the support you received. Your
feedback will help us improve our support.
For any concerns related to Intel® Developer Zone account, login or
website, please feel free to open a new ticket:
https://software.intel.com/en-us/support
这次 Intel Support 的反应挺快的,给个好评。就是希望 Intel 能够不挤牙膏,能拿出和 AMD 相当水平的 CPU。
最近发现空调遥控器电池有点不足,有时候会自动关机,于是拿出以前买的 Broadlink RM mini 3 充当远程的空调遥控器使用。为了方便手机上配置,分别采用了官方的 App 智慧星和 homebridge 进行配置。
首先用官方的智慧星配置好 Broadlink RM mini 3 的网络,然后配置 homebridge-broadlink-rm-pro。最早的插件作者不怎么更新了,这个版本是目前用的比较多的一个 fork。
安装好以后,在 Home 里面可以看到 Scan Code 的开关。打开以后,用遥控器在 Broadlink RM mini 3 附近按按键,就可以在 Homebridge 日志里看到 hex code 了。然后,就按照插件教程里的方法写配置,例子如下:
{
"platform": "BroadlinkRM",
"name": "Broadlink RM",
"accessories": [
{
"name": "Air Conditioner",
"type": "air-conditioner",
"noHumidity": true,
"minTemperature": 26,
"maxTemperature": 28,
"defaultCoolTemperature": 27,
"data": {
"off": "2600...",
"cool28": {
"data": "2600..."
},
"cool27": {
"data": "2600..."
},
"cool26": {
"data": "2600..."
}
}
}]
}
这样就可以在手机上方便地控制空调温度了。测试了一下,可以用 Siri 说“设置空调为 XX 度”,也是完全可以工作的。
P.S. 小米的空气净化器现在可以用插件 https://github.com/torifat/xiaomi-mi-air-purifier#readme,之前博客里写的那一个已经不更新了。
7.17 号周六的时候,一位朋友 @elenacliu 发现深研院网站的一个文档链接,文档中标题写的是 2021,但是网页的标题显示的是 2006 年。于是我发邮件给深研院的招生办,抄送本部的研招办,提交了这个 issue。
7.19 号周一上午的时候,本部的研招办回复了一封邮件,没有理解我想表达的意思,可能以为我是要报考的学生,让我关注明年发布的文档。
7.19 号周一下午的时候,深研院招生办回复了邮件,说“谢谢你的反馈”,不过没有提到是否进行了修复。我晚上再查看页面的时候,发现新的文档链接已经修复了问题。
最近一个服务忽然出现问题,用户反馈,HTTP POST 一个小的 body 不会出错,POST 一个大的 body 就会 500 Internal Server Error。
观察后端日志,发现没有出错的那一个请求。观察 Nginx 日志,发现最后一次日志是几个小时前。最后几条 Nginx 日志写的是 a client request body is buffered to a temporary file
。
继续研究后,发现是硬盘满了。Nginx 在处理 POST body 的时候,如果 body 超过阈值,会写入到临时文件中:
Syntax: client_body_buffer_size size;
Default: client_body_buffer_size 8k|16k;
Context: http, server, location
Sets buffer size for reading client request body. In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file. By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64. It is usually 16K on other 64-bit platforms.
详见 https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size
这就可以解释为什么 Nginx 返回 500 而且没有转发到后端,也可以解释为什么 Nginx 没有输出新的错误日志。
前段时间用 rook 搭建了一个 k8s 内部的 ceph 集群,但是使用过程中遇到了一些稳定性问题,所以想要用 cephadm 重建一个 ceph 集群。
重建的时候,我首先用 cephadm 搭建了一个 ceph 集群,再把原来的 MON 数据导入,再恢复各个 OSD。理论上,可能有更优雅的办法,但我还是慢慢通过比较复杂的办法解决了。
首先,配置 TUNA 源,在各个节点上安装 docker-ce
和 cephadm
。接着,在主节点上 bootstrap:
此时,在主节点上会运行最基础的 ceph 集群,不过此时还没有任何数据。寻找 ceph 分区,会发现因为 FSID 不匹配而无法导入。所以,首先要恢复 MON 数据。
参考文档:cephadm install。
首先,关掉 rook ceph 集群,找到留存下来的 MON 数据目录,默认路径是 /var/lib/rook
下的 mon-[a-z]
目录,找到最新的一个即可。我把目录下的路径覆盖到 cephadm 生成的 MON 目录下,然后跑起来,发现有几个问题:
/var/lib/ceph/$FSID
下第一个问题的解决办法就是临时用 MON 目录下的 keyring 进行认证,再创建一个新的 client.admin 认证。第二个问题的解决办法就是将遇到的各种 cephadm 生成的 FSID 替换为 MON 中的 FSID,包括目录名、各个目录下 unit.run 中的路径和 systemd unit 的名称。
进行一系列替换以后,原来的 MON 已经起来了,可以看到原来保留的各个 pool 和 cephfs 信息。
接下来,由于 MON 中保存的数据更新了,所以要重新生成 cephadm 的 SSH 密钥。将 SSH 密钥复制到各节点后,再用 cephadm 的 orch 功能部署到其他节点上。此时 FSID 都已经是 MON 中的 FSID,不需要替换。此时可以在 ceph orch ps
命令中看到在各个节点上运行的程序。接下来,还需要恢复各个 OSD。
为了从 ceph 分区从导出 OSD 的配置文件,需要用 ceph-volume
工具。这个工具会生成一个 /var/lib/ceph/osd-ID
目录,在 cephadm 的概念里属于 legacy,因此我们首先要把路径 mount 到 shell 里面:
接着,生成 osd 目录配置:
然后,可以看到创建了对应的 osd 路径,再用 cephadm 进行转换:
这样就可以用 cephadm 管理了。
配置好外部 ceph 集群后,还需要配置 k8s rook。
参考 https://rook.github.io/docs/rook/v1.8/ceph-cluster-crd.html#external-cluster,大概有这么几步:
有同学遇到这样的一个问题,代码中链接了一个第三方的动态库,在链接的时候出现了不一致的问题,比如有一个函数签名如下:
使用 GCC 11.1.0 编译上面的代码,可以发现它需要的符号是 _Z6foobarNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
,但是第三方库里面却是 _Z6foobarSs
,因此找不到对应的符号,链接失败。
经过一番研究,发现 Ss
在 Itanium ABI 中表示的是缩写:
In addition, the following catalog of abbreviations of the form "Sx" are used:
<substitution> ::= St # ::std::
<substitution> ::= Sa # ::std::allocator
<substitution> ::= Sb # ::std::basic_string
<substitution> ::= Ss # ::std::basic_string < char,
::std::char_traits<char>,
::std::allocator<char> >
<substitution> ::= Si # ::std::basic_istream<char, std::char_traits<char> >
<substitution> ::= So # ::std::basic_ostream<char, std::char_traits<char> >
<substitution> ::= Sd # ::std::basic_iostream<char, std::char_traits<char> >
这看起来很正常,_Z6foobarSs
表示的是 foobar(std::basic_string<char, std::char_traits<char>, std::allocator<char> >)
,但是 GCC 11.1.0 编译出来的上面的代码却没有用这个符号,而是 foobar(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
。差别就在于 __cxx11
中。
经过一番搜索,找到了 GCC 关于这个问题的文档和网上的文章,找到了原因:从 GCC5 开始,为了兼容 C++11 标准的改变,做了这个变动。如果要恢复原来的行为,需要添加一个定义:
$ g++ -D_GLIBCXX_USE_CXX11_ABI=0 -c test.cpp -o test.o && nm test.o | grep foobar
0000000000000000 T _Z6foobarSs
$ g++ -c test.cpp -o test.o && nm test.o | grep foobar
0000000000000000 T _Z6foobarNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
# install g++-4.9 in ubuntu 16.04
$ g++-4.9 -c test.cpp -o test.o && nm test.o | grep foobar
0000000000000000 T _Z6foobarSs
这样就可以正常链接到第三方的动态库了。
ATA 同时也是接口,图片如下。ATA 前身是 IDE,现在 ATA 叫做 PATA。
AHCI 可以简单理解为 PCIe <-> SATA 的转换器。AHCI 暴露为一个 PCIe 设备:
$ lspci -vv
00:1f.2 SATA controller: Intel Corporation C600/X79 series chipset 6-Port SATA AHCI Controller (rev 05)
Kernel modules: ahci
处理器通过 IO port/MMIO 访问 AHCI,然后 AHCI HBA 连接到 SATA 设备。
SATA 一般说的是接口。它一般分为两个部分,数据和电源。数据部分只有 7 个 pin,三个 GND 和两对差分线(A+A- B+B-),图片如下:
电源部分有 15 个 pin,有 GND 3.3V 5V 和 12V,图片如下:
常见的 SATA 盘有 2.5 英寸(small form factor, SFF)和 3.5 英寸(large form factor,LFF)两种规格。
M.2 又称 NGFF,有不同的 key 类型。常见的是 B 和 M:
都有部分引脚的位置是空的:
在这里可以看到两种 key 的 pinout。
可以看到,SATA pin 和 PCIe 的两个 lane 在 B 和 M key 中是一样的,物理上也是可以兼容的。
因为支持 SATA 和 PCIe,就有下面三种可能的使用方式:
SATA express 在 SATA 3.2 引入,它用的很少,被 U.2 取代。提供了 PCIe x2 或者 SATA x2。
U.2 也叫 SFF-8639。它和 SATA express 接口一样,但提供了 PCIe x4 或者 SATA x2。详见 pinout。
不同的协议的速度如下:
更完整的可以看List of interface bit rates。
Intel SSD DC P4618 Series 读写速度可以达到 40~50 Gb/s,它采用的是 PCIe 3.0 x8(64Gb/s) NVMe。
Intel SSD 545s Series 读写速度约 4Gb/s,采用的是 SATA 3.0 6Gb/s。
SAMSUNG 970 EVO 读写速度 20~30 Gb/s,它采用的是 PCIe 3.0 x4(32Gb/s) NVMe。
SAS 涉及的物理接口比较多,下面举一个具体的例子:DELL SCv2000
文档:https://dl.dell.com/topicspdf/storage-sc2000_owners-manual_en-us.pdf
它的背面:
它有四个前端接口 Mini-SAS High Density (HD),即 SFF-8644;两个后端接口 Mini-SAS,即 SFF-8088。
RAID 卡例子:MegaRAID SAS 9361-8i
文档:https://docs.broadcom.com/doc/12351995
它的接口有:
SAS 标准:
可以从 https://www.t10.org/drafts.htm#SCSI3_SAS 免费下载尚未成为标准的 SAS-4.1 Working Draft。
查找 SFF 标准:https://www.snia.org/technology-communities/sff/specifications
中文介绍:https://www.163.com/dy/article/H8TGPEUA0532B75P.html
Mini Multilane 4X Unshielded Connector Shell and Plug
介绍:https://cs-electronics.com/sff-8087/
Mini SAS 4i 连接器就是 36 pin 的 SFF-8087,支持四路 SAS。i 表示用于 internal 连接。对应的 external 接口是 SFF-8088。
标准下载地址:https://members.snia.org/document/dl/25823
它的引脚定义可以在 SFF-9402 看到,它的引脚分为 A 面和 B 面,每面有 18 个 PIN,用途如下:
这四组差分对对应四路 SAS 或者 SATA。SGPIO 协议的标准是 SFF-8485,主要用途是控制硬盘状态灯,以及判断盘是否插入。
相关标准:
Mini Multilane 4X Shielded Connector Shell and Plug
标准下载地址:https://members.snia.org/document/dl/25824
Mini SAS 4x 连接器就是 26 pin 的 SFF-8088,支持四路 SAS。用于 external 连接。对应的 internal 接口是 SFF-8087。
SFF-8482: Serial Attachment 2X Unshielded Connector (EIA-966)
介绍:https://cs-electronics.com/sff-8482/
支持两路 SAS,29 个引脚。和 SATA 的接口大小一样,目的是为了可以兼容 SATA 和 SAS 盘,比较常见。
标准下载地址:https://members.snia.org/document/dl/25920
不同速率的版本:
SFF-8614: Mini Multilane 4/8X Shielded Cage/Connector (HDsh)
标准下载地址:https://members.snia.org/document/dl/25939
对应的 internal 版本是 SFF-8643: Mini Multilane 4/8X 12 Gb/s Unshielded Connector
名称:External Mini-SAS HD(High Density)
升级版本:
SFF-8644: Mini Multilane 4/8X 12 Gb/s Shielded Cage/Connector (HD12sh)
标准下载地址:https://members.snia.org/document/dl/25952
支持 SAS-3 和 PCIe 3.0
Multifunction 6X Unshielded Connector
又称 U.2
标准下载地址:https://members.snia.org/document/dl/26489
用途:
MiniLink 4/8X I/O Cable Assemblies
又称 OCuLink 1.0
标准下载地址:https://members.snia.org/document/dl/27937
用途:
硬盘性能一般会看如下几个指标:
CrystalDiskInfo 术语:
根据 SSD vs HDD - Should You Buy a Solid State Drive or Hard Disk Drive?,SSD 和 HDD 在顺序读写性能上的常见范围:
SSD 和 HDD 在随机读写性能上的常见范围:
可以看到,SSD 和 HDD 相比,顺序读写性能高,随机读写性能显著高。但实际上,这个数据有些过时了。
下面看一些具体的例子:
最近有一台机器的盘出现了报警,需要换掉,然后重建 RAID5 阵列。iDRAC 出现报错:
首先,因为系统是 VMware ESXi 6.7,所以在DELL 官网下载对应的文件。按照里面的 README 安装 vib:
如果要升级系统,需要先卸载 vib:esxcli software vib remove -n vmware-perccli
,因为升级的时候会发现缺少新版系统的 perccli,建议先卸载,升级后再安装新的。
需要注意的是,如果复制上去 Linux 版本的 PERCCli,虽然也可以运行,但是找不到控制器。安装好以后,就可以运行 /opt/lsi/perccli/perccli
。接着,运行 perccli show all
,可以看到类似下面的信息:
$ perccli show all
--------------------------------------------------------------------------------
EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type
--------------------------------------------------------------------------------
32:2 2 Failed 1 3.637 TB SATA HDD N N 512B ST4000NM0033-9ZM170 U -
32:4 4 UGood F 3.637 TB SATA HDD N N 512B ST4000NM0033-9ZM170 U -
--------------------------------------------------------------------------------
其中 E32S2 是 Failed 的盘,属于 Disk Group 1;E32S4 是新插入的盘,准备替换掉 E32S2,目前不属于任何的 Disk Group。查看一下 Disk Group:perccli /c0/dall show
$ perccli /c0/dall show
-----------------------------------------------------------------------------
DG Arr Row EID:Slot DID Type State BT Size PDC PI SED DS3 FSpace TR
-----------------------------------------------------------------------------
1 - - - - RAID5 Dgrd N 7.276 TB dflt N N dflt N N
1 0 - - - RAID5 Dgrd N 7.276 TB dflt N N dflt N N
1 0 0 32:1 1 DRIVE Onln N 3.637 TB dflt N N dflt - N
1 0 1 32:2 2 DRIVE Failed N 3.637 TB dflt N N dflt - N
1 0 2 32:3 3 DRIVE Onln N 3.637 TB dflt N N dflt - N
可以看到 DG1 处于 Degraded 状态,然后 E32S4 处于 Failed 状态。参考了一下 PERCCli 文档,它告诉我们要这么做:
perccli /cx[/ex]/sx set offline
perccli /cx[/ex]/sx set missing
perccli /cx /dall show
perccli /cx[/ex]/sx insert dg=a array=b row=c
perccli /cx[/ex]/sx start rebuild
具体到我们这个情景,就是把 E32S2 设为 offline,然后用 E32S4 来替换它:
perccli /c0/e32/s2 set offline
perccli /c0/e32/s2 set missing
perccli /cx /dall show
perccli /cx/e32/s4 insert dg=1 array=0 row=2
perccli /cx/e32/s4 start rebuild
完成以后的状态:
TOPOLOGY :
========
---------------------------------------------------------------------------
DG Arr Row EID:Slot DID Type State BT Size PDC PI SED DS3 FSpace TR
---------------------------------------------------------------------------
1 - - - - RAID5 Dgrd N 7.276 TB dflt N N dflt N N
1 0 - - - RAID5 Dgrd N 7.276 TB dflt N N dflt N N
1 0 0 32:1 1 DRIVE Onln N 3.637 TB dflt N N dflt - N
1 0 1 32:4 4 DRIVE Rbld Y 3.637 TB dflt N N dflt - N
1 0 2 32:3 3 DRIVE Onln N 3.637 TB dflt N N dflt - N
---------------------------------------------------------------------------
可以看到 E32S4 替换了原来 E32S2 的位置,并且开始重建。查看重建进度:
$ perccli /c0/32/s4 show rebuild
-----------------------------------------------------
Drive-ID Progress% Status Estimated Time Left
-----------------------------------------------------
/c0/e32/s4 3 In progress -
-----------------------------------------------------
$ perccli show all
Need Attention :
==============
Controller 0 :
============
-------------------------------------------------------------------------------
EID:Slt DID State DG Size Intf Med SED PI SeSz Model Sp Type
-------------------------------------------------------------------------------
32:4 4 Rbld 1 3.637 TB SATA HDD N N 512B ST4000NM0033-9ZM170 U -
-------------------------------------------------------------------------------
然后,查看一下出错的盘:
$ perccli /c0/e32/s2 show all
Drive /c0/e32/s2 State :
======================
Shield Counter = 0
Media Error Count = 0
Other Error Count = 6
Drive Temperature = 36C (96.80 F)
Predictive Failure Count = 0
S.M.A.R.T alert flagged by drive = No
果然有错误,但是也看不到更多信息了。
坏块统计:
$ perccli /c0 show badblocks
Detailed Status :
===============
-------------------------------------------------------------
Ctrl Status Ctrl_Prop Value ErrMsg ErrCd
-------------------------------------------------------------
0 Failed Bad Block Count - BadBlockCount failed 2
-------------------------------------------------------------
经过检查以后,发现 E32S2 盘的 SMART 并没有报告什么问题,所以也没有把盘取走,而是作为 hot spare 当备用:
$ perccli /c0/e32/s2 add hotsparedrive DG=1
$ perccli /c0/d1 show
TOPOLOGY :
========
---------------------------------------------------------------------------
DG Arr Row EID:Slot DID Type State BT Size PDC PI SED DS3 FSpace TR
---------------------------------------------------------------------------
1 - - - - RAID5 Dgrd N 7.276 TB dflt N N dflt N N
1 0 - - - RAID5 Dgrd N 7.276 TB dflt N N dflt N N
1 0 0 32:1 1 DRIVE Onln N 3.637 TB dflt N N dflt - N
1 0 1 32:4 4 DRIVE Rbld Y 3.637 TB dflt N N dflt - N
1 0 2 32:3 3 DRIVE Onln N 3.637 TB dflt N N dflt - N
1 - - 32:2 2 DRIVE DHS - 3.637 TB - - - - - N
---------------------------------------------------------------------------
DG=Disk Group Index|Arr=Array Index|Row=Row Index|EID=Enclosure Device ID
DID=Device ID|Type=Drive Type|Onln=Online|Rbld=Rebuild|Optl=Optimal|Dgrd=Degraded
Pdgd=Partially degraded|Offln=Offline|BT=Background Task Active
PDC=PD Cache|PI=Protection Info|SED=Self Encrypting Drive|Frgn=Foreign
DS3=Dimmer Switch 3|dflt=Default|Msng=Missing|FSpace=Free Space Present
TR=Transport Ready
这样就可以做后备盘,当别的盘坏的时候,作为备用。
StorCLI:
MegaCLI:
PercCLI:
在维护一个 k8s 集群的时候,一个很常见的需求就是把日志持久化存下来,一方面是方便日后回溯,一方面也是聚合 replicate 出来的同一个服务的日志。
在我目前的需求下,只需要把日志持久下来,还不需要做额外的分析。所以我并没有部署类似 ElasticSearch 的服务来对日志进行索引。
实现主要参考官方的仓库:https://github.com/fluent/fluentd-kubernetes-daemonset。它把一些常用的插件打包到 docker 镜像中,然后提供了一些默认的设置,比如获取 k8s 日志和 pod 日志等等。为了达到我的需求,我希望:
由于 log server 不由 k8s 管理,所以按照官网的方式手动安装:
然后,编辑配置 /etc/td-agent/td-agent.conf
:
<source>
@type forward
@id input_forward
bind x.x.x.x
</source>
<match **>
@type file
path /var/log/fluentd/k8s
compress gzip
<buffer>
timekey 1d
timekey_use_utc true
timekey_wait 10m
</buffer>
</match>
分别设置输入:监听 fluentd forward 协议;输出:设置输出文件,和 buffer 配置。如有需要,可以加鉴权。
接着,按照 https://github.com/fluent/fluentd-kubernetes-daemonset/blob/master/fluentd-daemonset-forward.yaml,我做了一些修改,得到了下面的配置:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
namespace: kube-system
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: kube-system
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-system
labels:
k8s-app: fluentd-logging
version: v1
spec:
selector:
matchLabels:
k8s-app: fluentd-logging
version: v1
template:
metadata:
labels:
k8s-app: fluentd-logging
version: v1
spec:
serviceAccount: fluentd
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1-debian-forward
env:
- name: FLUENT_FOWARD_HOST
value: "x.x.x.x"
- name: FLUENT_FOWARD_PORT
value: "24224"
- name: FLUENTD_SYSTEMD_CONF
value: "disable"
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: config-volume
mountPath: /fluentd/etc/tail_container_parse.conf
subPath: tail_container_parse.conf
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: config-volume
configMap:
name: fluentd-config
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace: kube-system
data:
tail_container_parse.conf: |-
<parse>
@type cri
</parse>
和原版有几点细节上的不同:
部署到 k8s 中即可。为了保证日志的准确性,建议各个结点都保持 NTP 的同步。