跳转至

2020

用 Kubernetes 部署无状态服务

背景

最近需要部署一个用来跑编译的服务,服务从 MQ 取任务,编译完以后提交任务。最初的做法是包装成 docker,然后用 docker-compose 来 scale up。但既然有 k8s 这么好的工具,就试着学习了一下,踩了很多坑,总结了一些需要用到的命令。

搭建 Docker Registry

首先搭建一个本地的 Docker Repository,首先设置密码:

$ mkdir auth
$ htpasswd user pass > auth/passwd

然后运行 registry:

$ docker run -d -p 5000:5000 \
        --restart=always \
        --name registry \
        -v "$(pwd)/registry":/var/lib/registry \
        -v "$(pwd)/auth":/auth \
        -e "REGISTRY_AUTH=htpasswd" \
        -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
        -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
        registry:2

简单起见没有配 tls。然后吧本地的 image push 上去:

$ docker tag $image localhost:5000/$image
$ docker push localhost:5000/$image

这样就可以了。

搭建 k8s 环境

考虑到只用了单个物理机,所以采用的是 minikube。首先下载 minikube,下载方法略去。

接着新建 minikube 虚拟机:

$ minikube start --registry-mirror=https://registry.docker-cn.com --image-mirror-country=cn --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers --vm-driver=kvm2 --insecure-registry="0.0.0.0/0" --disk-size=50GB --cpus 128 --memory 131072

这里的 0.0.0.0/0 可以缩小,磁盘、CPU 和内存需要在这里就设好,之后不能改,要改只能重新开个虚拟机,不过这个过程也挺快的。

然后初始化一些组件(metrics server 和 kubernetes dashboard):

$ minikube addons enable metrics-server
$ minikube dashboard

如果要访问 dashboard 的话,可以用上面命令输出的链接,或者用 kubectl proxy 然后打开 http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ (注意 http 还是 https)。

如果问到 Access Token,可以用以下 alias 获得(fish/macOS):

$ alias kubedashboard="kubectl -n kubernetes-dashboard describe secret (kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print \$1}') | tail -n1 | awk '{print \$2}' | pbcopy"

接着,配置一下 docker registry 的密钥:

$ kubectl create secret generic regcred --from-file=.dockerconfigjson=/path/to/config.json  --type=kubernetes.io/dockerconfigjson

然后,在 Pod/Deployment 里面设定镜像:

containers:
  - name: name
    image: IP:5000/image
imagePullSecrets:
  - name: regcred

然后部署即可。

部署水平自动伸缩(HPA)

这一步配置的是自带的 HPA 功能,需要上述的 metrics-server 打开,并且在 Pod/Deployment 里面写明 resources.requests.cpu:

- name: name
  resources:
    requests:
      cpu: "xxx"

然后创建 HPA 即可:

$ kubectl autoscale deployment $deployment --cpu-percent=50 --min=1 --max=10

通过压测,可以看到自动伸缩的记录:

$ kubectl describe hpa
Normal  SuccessfulRescale  22s   horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal  SuccessfulRescale  6s     horizontal-pod-autoscaler  New size: 1; reason: All metrics below target

参考:Kubernetes 官方文档

用 jailkit 限制用户仅 scp

最近需要用 scp 部署到生产机器,但又不想出现安全问题,所以用了 jailkit 的方法。首先是创建单独的用户,然后生成 ssh key 来认证,不再赘述。此时是可以 scp 了,但用户依然可以获得 shell,不够安全。

然后找到了下面参考链接,大概摘录一下所需要的命令和配置:

mkdir /path/to/jail
chown root:root /path/to/jail
chmod 701 /path/to/jail
jk_init -j /path/to/jail scp sftp jk_lsh
jk_jailuser -m -j /path/to/jail jailed_user
vim /path/to/jail/etc/jailkit/jk_lsh.ini
# Add following lines
[jailed_user]
paths = /usr/bin, /usr/lib
exectuables = /usr/bin/scp

之后可以发现该用户的 shell 已经更改 jk_chrootsh,并且只能用 scp。

参考:https://blog.tinned-software.net/restrict-linux-user-to-scp-to-his-home-directory/

每周分享第 56 期

咕咕咕

  1. SystemVerilog linter https://github.com/dalance/svlint
  2. 东北方言编程语言 https://github.com/zhanyong-wan/dongbei
  3. JS LaTeX 渲染到 HTML https://github.com/michael-brade/LaTeX.js
  4. 一种对语音助手的攻击 https://surfingattack.github.io/
  5. 在线打铃网站 http://thulpwan.net/timer/
  6. 网络学堂 PC 端 App https://github.com/jiegec/learn_tsinghua_app/releases
  7. Rust 2020 roadmap https://github.com/rust-lang/rfcs/pull/2857/files

通过 BSCAN JTAG 对 Rocket Chip 进行调试

前言

在上一个 post 里研究了原理,今天也是成功在 Artix 7 上实现了调试。效果如下:

OpenOCD 输出:

Info : JTAG tap: riscv.cpu tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Info : datacount=1 progbufsize=16
Info : Disabling abstract command reads from CSRs.
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40801105
Info : Listening on port 3333 for gdb connections

GDB 输出:

Remote debugging using localhost:3333
0x0001018c in getc () at bootloader.c:36
36        while (!(*UART_LSR & 0x1))
(gdb) 

这里用的 OpenOCD 和 GDB 都是 riscv 版本,上游的支持尚不完善。对于 Homebrew 用户,我在 jiegec/homebrew-formulas 维护了需要的 Formula。

过程

代码基本借鉴了 sequencer/rocket-playgroundKireinaHoro/rocket-zcu102 而来,代码方面主要是添加了 BscanJTAG.scala,然后在 Top 模块下把它连接到内部的 JTAG 中:

val boardJTAG = Module(new BscanJTAG)
val jtagBundle = target.debug.head.systemjtag.head

// set JTAG parameters
jtagBundle.reset := reset
jtagBundle.mfr_id := 0x233.U(11.W)
jtagBundle.part_number := 0.U(16.W)
jtagBundle.version := 0.U(4.W)
// connect to BSCAN
jtagBundle.jtag.TCK := boardJTAG.tck
jtagBundle.jtag.TMS := boardJTAG.tms
jtagBundle.jtag.TDI := boardJTAG.tdi
boardJTAG.tdo := jtagBundle.jtag.TDO.data
boardJTAG.tdoEnable := jtagBundle.jtag.TDO.driven

代码方面就足够了。然后,需要一个 riscv-openocd 和 riscv-gdb,分别从上游 repo 编译得来。然后采用以下的 openocd.cfg:

adapter_khz 20000
interface ftdi
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x00e8 0x60eb
ftdi_tdo_sample_edge falling
reset_config none

set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 6

set _TARGETNAME $_CHIPNAME.cpu

target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME
$_TARGETNAME.0 configure -work-area-phys 0x80000000 -work-area-size 10000 -work-area-backup 1
riscv use_bscan_tunnel 5

然后就可以用 GDB 调试了。

在 Vivado 中对 chisel3 产生的 verilog 代码仿真

默认情况下,chisel3 生成的 verilog 代码在 Vivado 中仿真会出现很多信号大面积变成 X。解决方法在一个不起眼的 Wiki 页面:Randomization flags

`define RANDOMIZE_REG_INIT
`define RANDOMIZE_MEM_INIT
`define RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE_INVALID_ASSIGN

在生成的 verilog 前面加上这四句,就可以正常仿真了。

在 macOS 烧写 Artix7 FPGA

首先安装好 openocd:

brew install openocd --HEAD

测试所用版本为 0.10.0+dev-01052-g09580964 (2020-02-08-15:09)

然后编写如下的 openocd.cfg:

adapter driver ftdi
adapter speed 10000
ftdi_vid_pid 0x0403 0x6014
ftdi_layout_init 0x0008 0x004b

source [find cpld/xilinx-xc7.cfg]
init
xc7_program xc7.tap
pld load 0 /path/to/bitstream.bit
shutdown

上面的 ftdi 开头的两行按照实际的 JTAG Adapter 修改。可以参考 openocd 自带的一些 cfg。

然后在 openocd.cfg 的目录运行 openocd 即可:

$ openocd
Open On-Chip Debugger 0.10.0+dev-01052-g09580964 (2020-02-08-15:09)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 10000 kHz
Info : JTAG tap: xc7.tap tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Warn : gdb services need one or more targets defined
shutdown command invoked
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

这时 FPGA 已经烧写成功。

参考:

  1. https://pansila.github.io/posts/7db4884d
  2. https://numato.com/kb/programming-mimas-a7-using-openocd-and-xc3sprog/

更新:OpenOCD 已经更新到 0.11.0,对于 Arty A7,采用下面的脚本进行烧写:

# OpenOCD 0.11.0
# Adapted from: interface/ftdi/digilent-hs1.cfg
# See also: board/arty_s7.cfg
adapter driver ftdi
adapter speed 25000
ftdi_vid_pid 0x0403 0x6010
ftdi_channel 0
ftdi_layout_init 0x0088 0x008b
reset_config none

source [find cpld/xilinx-xc7.cfg]
init
pld load 0 ./bitstream.bit
shutdown

成功输出:

$ openocd
Open On-Chip Debugger 0.11.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'.
Info : ftdi: if you experience problems at higher adapter clocks, try the command "ftdi_tdo_sample_edge falling"
Info : clock speed 25000 kHz
Info : JTAG tap: xc7.tap tap/device found: 0x0362d093 (mfg: 0x049 (Xilinx), part: 0x362d, ver: 0x0)
Warn : gdb services need one or more targets defined
shutdown command invoked
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

研究 Rocket Chip 的 BSCAN 调试原理

前言

最近 @jsteward 在研究如何通过 JTAG 对 FPGA 里的 Rocket Chip 进行调试。之前 @sequencer 已经做了一些实践,我们在重复他的工作,同时也研究了一下这是怎么工作的。

原理

我们从 @sequencer 得到了一份可用的 Scala 代码OpenOCD 配置,并且了解到:

  1. 可以通过 openocd 找到并调试 Rocket Chip
  2. openocd 是通过 JTAG 向 FPGA 的 TAP 的 IR 写入 USER4,然后往 DR 写入特定格式的数据,然后控制 Rocket Chip 的 JTAG。

这里涉及到一个“封装”的过程,在一个仅可以控制 DR 的 JTAG 中控制另一个 JTAG。首先可以找到 OpenOCD 端的操作代码

tunneled_ir[3].num_bits = 3;
tunneled_ir[3].out_value = bscan_zero;
tunneled_ir[3].in_value = NULL;
tunneled_ir[2].num_bits = bscan_tunnel_ir_width;
tunneled_ir[2].out_value = ir_dtmcontrol;
tunneled_ir[1].in_value = NULL;
tunneled_ir[1].num_bits = 7;
tunneled_ir[1].out_value = tunneled_ir_width;
tunneled_ir[2].in_value = NULL;
tunneled_ir[0].num_bits = 1;
tunneled_ir[0].out_value = bscan_zero;
tunneled_ir[0].in_value = NULL;

如果画成图,大概是这个样子(IR):

3 bits IR Width bits 7 bits 1 bit TDI Data Register TDO
0 Payload Tunneled IR Width 0 -> Rocket Chip TAP ->

DR:

3 bits DR Width bits 7 bits 1 bit TDI Data Register TDO
0 Payload Tunneled DR Width 1 -> Rocket Chip TAP ->

这里 TDI 和 TDO 是直接接到 Rocket Chip 的 JTAG 中的,所以我们期望,当 Rocket Chip TAP 在 Shift-IR/Shift-DR 阶段的时候,刚好通过的是 Payload 部分。而控制 TAP 状态机,需要控制 TMS,这个则是通过一段 HDL 来完成的:

always@(*) begin 
        if (counter_neg == 8'h04) begin 
                jtag_tms = TDI_REG; 
        end else if (counter_neg == 8'h05) begin 
                jtag_tms = 1'b1; 
        end else if ((counter_neg == (8'h08 + shiftreg_cnt)) || (counter_neg == (8'h08 + shiftreg_cnt - 8'h01))) begin 
                jtag_tms = 1'b1; 
        end else begin 
                jtag_tms = 1'b0; 
        end 
end

这里 TDI_REG 取的是第一个 bit 的反(也就是上面 IR 对应 0,DR 对应 1 的那一位),shiftreg_cnf 则是之后 7 个 bit,对应上面的 Tunneled IR/DR Width。那么,在选择 IR 时 TMS 的序列为:

4 cycles 1 cycle 1 cycle 2 cycles shiftreg_cnt-1 cycles 2 cycles rest cycles
0 1 1 0 0 1 0
Run-Test/Idle Select-DR-Scan Select-IR-Scan Capture-IR, Shift-IR Shift-IR Exit1-IR, Update-IR Run-Test/Idle

类似地,如果是选择 DR:

4 cycles 1 cycle 1 cycle 2 cycles shiftreg_cnt-1 cycles 2 cycles rest cycles
0 0 1 0 0 1 0
Run-Test/Idle Run-Test/Idle Select-DR-Scan Capture-DR, Shift-DR Shift-DR Exit1-DR, Update-DR Run-Test/Idle

这样,刚好在 Shift-IR/DR 状态下,Payload 会被写入 IR/DR,从而完成了期望的操作。通过规定一个特定格式的 Data Register,可以实现嵌套的 TAP 的 IR 和 DR 的操作。

参考

  1. JTAG Standard
  2. sequencer/rocket-playground
  3. SiFive's JTAG Tunnel: https://github.com/sifive/fpga-shells/blob/c099bd9b4f916bc0ba88030939a9614d0b0daf2d/src/main/scala/ip/xilinx/Xilinx.scala#L13
  4. https://github.com/watz0n/arty_xjtag
  5. https://github.com/riscv/riscv-openocd/blob/7cb8843794a258380b7c37509e5c693977675b2a/src/target/riscv/riscv.c#L361
  6. UG740: 7 Series FPGAs Configuration

在 macOS 上带执行权限 mmap 一个已删除文件遇到的问题和解决方案

背景

实验环境:macOS Catalina 10.15.2

最近在 rcore-rs/zircon-rs 项目中遇到一个比较玄学的问题,首先需求是在 macOS 的用户进程里开辟一段地址空间,然后把这个地址空间多次映射(权限可能不同、同一块内存可能被映射到多个地址),通过 mmap 模拟虚拟地址的映射。采用的是如下的方案:

  1. 在临时目录创建一个文件,把文件大小设为 16M(暂不考虑扩容)
  2. 需要映射一个虚拟地址到物理地址的时候,就对这个文件的物理地址偏移进行 FIXED 映射,虚拟地址就是期望的虚拟地址。

这样的方案在 Linux 下运行地很好,但在 macOS 下总是以一定概率在第二部出现 EPERM。网上搜了很多,但也没搜到相关的信息,于是自己断断续续地研究了一下,现在有一个比较初步的结果。

TL;DR

先说结论:调用一个带 PROT_EXEC 并且映射文件的 mmap 时,macOS 会进行安全检测,如果此时发现文件在文件系统上消失了,它会认为这可能是一个恶意软件行为,进行拦截,返回 EPERM。

而代码实际上在第一步和第二步之间,把临时目录删了:由于进程持有 fd,所以文件并不会真的删掉,当软件退出的时候文件自然会删除,这是临时文件的常见做法(见 tmpfile(3))。

研究过程

查看 Console

在网上一番搜索未果后,就尝试在 Console 里面寻找信息。照着程序名字搜索,可以找到一些信息:

temporarySigning type=1 matchFlags=0x0 path=/path/to/executable

这是编译这个 executable 的时候出现的,好像也没啥问题。然后解除过滤,在这个信息前后按照 syspolicyd 寻找:

initiating malware scan (... info_path: /path/to/temp/file proc_path: /path/to/executable)
Unable (errno: 2) to read file at <private> for process path: <private> library path: <private>
Disallowing load of <private> in 50001, <private>
Library load (/path/to/temp/file) rejected: library load denied by system policy

这几条记录比较可疑,每次运行程序,如果跑挂了,就会出现这几条,如果没跑挂,就不会出现这一条。所以很大概率是被 macOS 拦截了。错误信息的用词是 library,所以大概率是被当成加载动态库了,但既然内容是空的,所以我想的是文件名触碰到了什么奇怪的规则,然后文件名又是随机的,随机导致 EPERM 是概率性出现的,这好像很有道理。于是我把 tmpfile 换成了固定的路径,忽然就好了。但固定的路径只能保证同时只有一个程序在跑,如果路径拼接上 pid,怎么删,谁来删又是一个问题。虽然可以放到 /tmp 下面然后随便搞,但 /tmp 的回收并不是那么积极,在临时目录下丢太多文件也会出现问题。

一丝曙光

这时候,@wangrunji0408 提供了一个方案:在 System Preferences -> Security & Privacy -> Privacy -> Developer Tools 中添加编译该 executable 的程序(如 iTerm、CLion)可以解决这个问题。那么问题应该比较明确了,就是 malware scan 的问题,如果信任了这个 App 为 Developer Tools,它产生的 executable 也是可信的,应该不是恶意软件。但在 tmux 环境下,它哪个 App 也不属于,没法继承,况且把这个权限开放出去也有潜在的安全问题。并且让每个开发者都要这么操作一遍很不方便。

回到 Console

今天刚好看到一个 post,内容是如何在 macOS Catalina 中查看 log 中标记为 private 的内容。如果你注意到的话,上面的 log 中出现了几处 private,这并不是我改的,而是 macOS 自带的隐私机制(当然这种机制似乎并没有采用的很完全,一些消息源没有打上 private 的标签)。

然后按照上面的 post 的方法(另一个 post)开启了一下标记为 private 的内容,正好我的系统没有升级到 10.15.3 所以还能用。此时上面的第二条和第三条就出现了具体内容:

Unable (errno: 2) to read file at /path/to/temp/file for process path: /path/to/executable library path: /path/to/temp/file
Disallowing load of /path/to/temp/file in 61254, /path/to/executable

这个时候问题就很明显了:读取不到文件。这时候回想起 tmpfile 的工作原理,它会删除生成的文件,在删除文件之后,macOS 进行扫描,发现找不到文件,于是 disallow 了,mmap 就会返回 EPERM。

解决方案也很显然了:把删除目录延后,或者放在 /tmp 下等待清理等待。

我也写了一段 C 代码来验证这个现象:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>


int main() {
    int fd = open("mmap", O_RDWR | O_CREAT, 0777);
    uint64_t addr = 0x200000000;
    ftruncate(fd, 16*1024*1024);
    // might not work if unlink is put here (race condition)
    // you can use sleep to reproduce
    unlink("mmap");
    void * res = mmap((void *)addr, 16*1024*1024, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_FIXED, fd, 0);
    // always works if unlink is put here
    // unlink("mmap");
    if (res == MAP_FAILED) {
        perror("mmap");
    } else {
        printf("good");
    }
    return 0;
}

每周分享第 55 期

一个月后终于复更

  1. 退出 vim 教程 https://github.com/hakluke/how-to-exit-vim
  2. SHA-1 攻击新进展 https://sha-mbles.github.io/
  3. gmane 近况 https://lars.ingebrigtsen.no/2020/01/06/whatever-happened-to-news-gmane-org/
  4. 浏览器能做的事情 https://github.com/luruke/browser-2020
  5. 一个 ext2 和 FAT 为一体的 fs https://github.com/NieDzejkob/cursedfs
  6. iptables 规则调试工具 https://github.com/x-way/iptables-tracer
  7. Qt 2020 的变化 https://www.qt.io/blog/qt-offering-changes-2020
  8. 后缀自动机可视化 https://yeah.moe/p/a8e74947/

MacBookPro 14,3 Wi-Fi 驱动问题解决方案

之前在 MacBookPro 14,3 安装 Linux 后,很多东西的驱动都有了解决方法,参考 1参考 2,触摸板和键盘等等都可以正常使用,触摸板的使用效果比我意料要好一些,虽然还是没有 macOS 原生那么好。但 Wi-Fi 一直有能扫到信号但连不上的问题,最近终于有了比较完善的解决方案,地址,也是两个月前才出来的方案,我测试了一下,确实可以很好的解决网络问题,网卡型号是 BCM43602,驱动用的是 brcmfmac。

另一方面,带 T2 的 MacBook 似乎也有了支持,见 aunali1/linux-mbp-arch,有一些尚未 upstream 的 patch,但我没有设备,就没有测试了。需要吐槽的是 ArchWiki 不怎么更新新 Model 的 MacBook 的教程,都是到处找散落的 github repo 和 gist 找别人的方案。

P.S. 可以正常工作的有:Wi-Fi,键盘,触摸板,Touchbar,内置摄像头,键盘背光,蓝牙 P.P.S MacBookPro11,2 的网卡是 BCM4360,直接用 broadcom-wl 驱动就可以。

JieLabs 是如何工作的

简介

JieLabs 是陈嘉杰、高一川、刘晓义(按姓氏拼音首字母排序)于 2020 年新型冠状病毒疫情期间开发的在线数字逻辑电路实验系统,用于清华大学 2020 年春季学期数字逻辑电路实验课程。其包括前端、后端和固件三部分,分别主要由刘晓义、陈嘉杰和高一川负责开发。核心功能实现用时一周,后续界面和稳定性优化用时两周。本文会详细地介绍 JieLabs 的工作原理和一些技术细节,希望对各位同学有所帮助。

太长;不看。

采用了如下的技术方案:

前端:React 框架 + Redux 状态管理 + Monaco 编辑器 + WebAssembly 运行 Rust 代码 + WebSocket 实时通信 + SASS 样式

后端:Actix-Web 框架 + Diesel/PostgreSQL 数据库 + Redis 消息队列 + Quartus 构建 + Kubernetes 构建容器编排

固件:Xilinx FPGA 控制 + Buildroot 系统 + Linux 内核

前端

前端大部分都是刘晓义同学编写的,也是这个项目工作量最大的一部分。除了本文,还可以阅读刘晓义同学自己写的总结。主要分以下几部分来谈前端的技术实现:

第三方库

整体上采用了时下比较流行的 React 框架,配合 Redux 进行状态管理,用 React Hooks 编写组件的逻辑。为了实现 VHDL/Verilog 代码的编辑,采用了来自 VSCode 的独立编辑器空间 Monaco,并自行编写了 VHDL 和 Verilog 语言的支持,一部分在 JS 实现,另一部分在 Rust 中实现,通过 wasm-pack 打包到 JS 中执行。另外为了实现 gzip 格式的解压缩也引入了 pako 库。

在这些第三方库里,Monaco 的体积最大,后面我们针对 JS 体积做了许多优化,在下面会再提。

Rust 在前端的应用

由于开发者里刘晓义和陈嘉杰都是 Rust 语言的爱好者,考虑到目前 Rust to WASM 的技术比较成熟,WebAssembly 的可用程度也很高,我们把一些功能挪到了 Rust 中执行:

一是布线的计算。这是一个比较纯粹的算法问题,一方面对性能有一定的要求,一方面开发者比较喜欢 Rust,所以就用 Rust 实现了。这里要特别感谢刘光哲同学对布线算法的指点。在此基础上,我们用 Rust 实现了几个论文中的布线算法(Maze Router),并且通过和 JS 代码的配合得到了一个比较优秀的效果。

二是 VHDL/Verilog 的语言支持。学过编译原理的同学应该知道,如果要实现一个能够解析代码里的信号的程序,一般是不能通过正则表达式来解决的,况且我们还实现了一些错误信息的显示。VHDL 语言支持采用了已有的比较完善的库,Verilog 由于现有的库都比较庞大,不适合放于前端,于是我们编写了一个最小的 Verilog(实际上算是 SystemVerilog)的解析,仅仅足够满足我们的需求。如果同学们遇到了一些语法上功能的缺少,欢迎提出。

Canvas 的应用

连线部分因为是动态生成的,所以也是动态绘制的,Canvas 就可以派上用场了。我们也利用了 Canvas 的特性,针对每一个网络都画在一个 Canvas 上,那么在检测鼠标位置的时候,只要检查 Canvas 在鼠标所在的点上是否颜色,就知道鼠标是否在它上面了。

前端加载速度的优化

优化前前端 JS 和 WASM 总大小大约是 4MB,对于网络不好的用户来说,它的加载时间是不能容忍的。于是我们采用了以下的措施:

  1. 打开 gzip:有很显著的效果,但因为一些未知的原因,在实际部署的时候未能打开
  2. 缩小 JS 体积:通过 Webpack Analyzer 分析程序各个部分的大小,删掉了 Monaco 中一些没有用到的功能
  3. 缩小 WASM 体积:打开 LTO 和 -Os 选项
  4. 代码分割:把不同功能的代码分割开,先让一部分代码加载进来,可以绘制一个部分功能的界面,然后再继续加载剩下的组件
  5. CDN:把一部分外部的依赖放到国内,后续如果有需求的话也可以把内部的依赖也放到国内的 CDN 上

后端

后端用 Rust 语言编写,采用了目前比较成熟的 actix-web 框架,大量使用了 async/await。除此之外,用 Redis 作为消息队列,在 Docker 容器中运行 Quartus,用 Kubernetes 进行容器的动态调度。

任务调度

对于用户提交的代码和约束,后端需要进行任务的调度,生成一个新的任务,放入到 Redis 消息队列中。另一方面,Docker 中运行的 python 脚本会从 Redis 中取任务,任务完成后把结果上传并回传给后端表示确认。如果一个任务一直没有完成,后端会进行回收并重新分配一个任务到队列中。为了防止这个过程中出现重复任务的提交,为每个提交设置了一个足够长的随机 ID。Docker 容器一开始是通过 docker-compose 进行配置,后来考虑到这个场景比较适合 kubernetes,于是使用了一下,还挺好用的。一开始用的是 minikube,搭好 docker registry,然后往里面部署几个 pod 并设置 hpa,具体可以看我的另一篇博客,后来迁移到了 kubeadm 直接配置。现在迁移到了一个 k3s 搭建的 k8s 集群上。

板子通信

第二个主要功能是进行板子的分配和通信。每个用户会创建一个 WebSocket 连接到后端,每个板子也是一个 WebSocket。当一个用户分配到一个板子的时候,它可以通过后端发送命令给对应的板子,板子的回复也会原路返回,相当于一个 WebSocket Proxy。另外为了保证资源的利用率,添加了一些限制、心跳包和认证。

状态监控

为了可以直观地看到各个数据,实现了一个简单的监控接口,接入 Telegraf+InfluxDB+Grafana 的监控系统,可以实时看到各个资源的情况,如用户、板子和任务等等,也方便我们在在线用户比较少的时候进行更新。

板子

这个平台虽然是用于数字逻辑实验课程,但实际用的板子来自数字逻辑设计课程。我们把其上一个 Altera FPGA 作为实验 FPGA,在控制的 Xilinx FPGA 上运行我们的固件,负责读取和写入 GPIO、下载 bitstream 等等功能。