我用Nim语言制作的个人网页应用已经运营了一年

引言

我在2019年的Nim圣诞日历上发布了以下文章。
使用Nim创建并发布了一个Web应用程序。

从那以后的一年里,更新频率明显降低了,但不管怎样,目前仍在进行维护运营。
我将写下使用一年后的感受和发生变化的地方。

网站

这里是网页的界面:
https://websh.jiro4989.com/

源代码

我已将基础设施的代码设置为私密。

开发环境的定义

    • Nim 1.4.0

 

    Ubuntu 18.04

手机应用程序

使图像文件可以上传。

元になっている是一个称为Shell艺术机器人的Twitter机器人,在那个机器人中,可以上传图像文件并将其纳入Shell艺术机器人的容器内。

根据此进行了允许导入图像的调整。

在发布推特时可以切换标签。

我们前端采用了Karax。
以下是我们前端Karax的代码。

在选择元素的块内,通过onchange事件更新hashTag,并通过循环创建option,选择框的实现就完成了。

只需一个选项,将以下内容从日语直译成中文:通过对”Tweet”按钮的URL进行百分号编码并嵌入hashTag,标签切换的实现已经完成。我记得这一点实现起来比想象中要简单。

tdiv(class = "buttons"):
  button(class="button is-primary", onclick = sendShellButtonOnClick):
    text "Run (Ctrl + Enter)"
  a(href = &"""https://twitter.com/intent/tweet?hashtags={encodeUrl($hashTag, false)}&text={encodeUrl($inputShell, false)}&ref_src=twsrc%5Etfw""",
      class = "button twitter-share-button is-link",
      `data-show-count` = "false"):
    text "Tweet"
  tdiv(class = "select"):
    select:
      proc onchange(ev: Event, n: VNode) =
        hashTag = $n.value
      for elem in @[cstring"シェル芸", cstring"shellgei", cstring"ゆるシェル", cstring"危険シェル芸", ]:
        option(value = $elem):
          text $elem

将执行过的Shell命令历史保存到本地存储(LocalStorage)。

我們現在可以顯示最多20個以前執行過的Shell歷史記錄。這些記錄將保存在本地存儲(LocalStorage)中。

由于Karax提供支持,因此可以通过import karax/localstorage进行访问。

这段代码如下:仅将数据以JSON格式保存在本地存储中。

# LocalStorageからの取得部分

# localstorageにシェルの履歴が存在するときだけ取得
if localstorage.hasItem("history"):
  let hist = localstorage.getItem("history").`$`.parseJson.to(seq[string])
  shellHistory.add(hist)

# LocalStorageへの保存部分

proc sendShellButtonOnClick(ev: Event, n: VNode) =
  ## Tweetボタンを押した時に呼び出されるプロシージャ

  # 省略

  localStorage.setItem("history", shellHistory.mapIt(cstring(it)).toJson)

在构建应用程序时将标签和提交哈希嵌入到HTML中。

为了使画面上的源代码提交哈希可见,我在构建过程中进行了嵌入。这在JS构建中也是可能的。

在 Nim 中,您可以通过在代码中添加 strdefine 指令来在构建时嵌入值。
您还可以通过 intdefine 来嵌入整数类型,通过 booldefine 来嵌入布尔类型。

# コンパイル時に値を埋め込む
const tag {.strdefine.} = "v0.0.0"
const revision {.strdefine.} = "develop"

使用上述的预设宏设置,通过在构建时传递选项来完成嵌入。
可以使用 “nimble build -d:<变量名称>:<值>” 的方式进行嵌入。

由于我们已经在CI中进行了自动化发布,所以我们已经在下面的GitHub Actions步骤中嵌入了构建过程。

build-artifact:
  runs-on: ubuntu-latest
  needs: before
  steps:
    - uses: actions/checkout@v2

    - name: Build assets
      run: |
        docker build --target base -t base .
        docker run --rm -v $PWD/websh_front:/work -t base \
          nimble build -Y \
                       "-d:tag:${GITHUB_REF/refs?heads?}" \
                       "-d:revision:$GITHUB_SHA"

请使用Docker API

我們在容器化中使用DockerAPI將Shell芸bot容器操作的方式改為使用。

起初,Shell艺术bot的容器是通过调用docker命令进行操作的。然而,为了从容器内部操作主机的容器,需要将其容器化并配置为Docker in Docker结构。

在Docker中使用Docker的构建必须使用dind镜像。
虽然可以基于dind镜像来安装Nim编译器,
但是版本管理可能会变得很麻烦。

因此,我们使用Docker API来操作容器,并确保其在Nim官方的Docker镜像基础上正常运行。

要使API请求能够操作Docker,需要改变Docker的设置。我已经根据以下方式修改了使用systemd启动Docker的启动设置。

由于在本地进行开发时也需要,所以我只能接受这一点繁琐的过程。

# ここを
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

# こう修正
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2376 -H fd:// --containerd=/run/containerd/containerd.sock

同时,至今尚无维护的用于在Nim中通过API操作Docker的库。因此,我们只能根据Docker API的规范参考自行实现所需的API。

stdout、stderr的日志获取部分的实现很繁琐。
无法直接使用字符串读取,所以我自己解析它们。

proc parseLog(s: string): string =
  var strm = newStringStream(s)
  defer: strm.close
  var lines: seq[string]
  while not strm.atEnd:
    # 1 = stdout, 2 = stderr
    if strm.readUint8() notin [1'u8, 2]:
      break

    # 3byteは使わないので捨てる
    discard strm.readUint8()
    discard strm.readUint8()
    discard strm.readUint8()

    # Bigendianで読み取る
    var src = strm.readUint32().int
    var n: int
    bigEndian32(addr(n), addr(src))

    lines.add(strm.readStr(n))
  result = lines.join

基础设施

容器化

最初,我們上傳了編譯好的二進制文件到伺服器上並直接在伺服器上執行。後來,我們希望將其容器化,所以現在正在使用docker-compose來運行容器。

由于最初并不是为容器而设计,所以进行了相当大的改造工作。
https://github.com/jiro4989/websh/pull/173

在将应用容器化之前,本地环境的启动需要进行各种环境配置,启动服务器也需要遵循特定的顺序,非常麻烦。但是自从应用容器化完成后,只需使用docker-compose up命令即可启动环境,大大简化了流程。

使用Prometheus和Grafana进行服务监控。

当初我写 Advent Calendar 的时候,几乎没有进行服务监控;然而现在我开始使用 Prometheus + Grafana + Grafana Loki 进行服务监控。虽然目前还没有进行严格的监控,但这给人一种安心感。

将日志转换为JSON格式

我們使用Grafana + Grafana Loki來監控日誌。
由於我們不想自行開發解析日誌的程式,所以我們將日誌格式更改為JSON格式,以便使用內建的JSON解析器。
我們在應用程式中進行了修改以進行相應的處理。

我只是简单地使用JSON模块将其转换为JSON并直接输出到标准输出。虽然我想将其作为日志记录器提取出来,但觉得麻烦就没有这样做。

    const
      # #158 JSONのキーにハイフンを含めるべきでないのでlowerCamelCaseにする
      xForHeader = "xForwardedFor"
    let
      now = now()
      uuid = $genUUID()
      xFor = request.headers().getOrDefault("X-Forwarded-for")
    try:
      var respJson = request.body().parseJson().to(ReqShellgeiJSON)

      # 一連の処理開始のログ
      echo %*{xForHeader: xFor, "time": $now(), "level": "info", "uuid": uuid, "code": respJson.code, "msg": "request begin"}

通过使用ansible-galaxy,可以避免手动编写中间件的安装和配置描述。

过去,我自己编写中间件的安装配置,但现在我开始使用ansible-galaxy来使用第三方Ansible角色。

现在完全不需要编写Prometheus Exporter或Loki相关中间件的安装设置。

将服务管理从主管者统一到systemd中。

最初我們使用supervisor來管理應用程序,但為了統一,我們現在全部改用systemd。原因如下:

    • ansible-galaxyを使いたかった

3rd partyのansible galaxyを利用する場合、大多数がsystemdでサービスを起動するように作られている

systemd-journalでログを収集したかった

Grafana Lokiにログを流すとき手段がいくつかありました
手段

fluentdでログを拾ってpromtailに流す
systemd-journalからpromtailに流す

このときsystemd-journalからログを拾う場合だとfluentdをサーバ上に追加でインストールする必要がなく、管理が楽になると判断しました

また、後述するLoki上でデータを分析するためのリラベリングもやりやすかったため

使用Grafana Loki将日志可视化并进行筛选。

在Grafana Loki的界面上对日志进行过滤,需要对日志进行标签化。

有许多方法可以对日志进行标记,但如果日志是从systemd-journal以JSON格式传输的,则使用Promtail进行标记设置会变得非常简单。

以下是Promtail的scrape_configs:


scrape_configs:
  - job_name: journal
    journal:
      max_age: 12h
      labels:
        job: systemd-journal
    relabel_configs:
      - source_labels: ['__journal__systemd_unit']
        target_label: 'unit'
      # session-* というラベルが別で割り振られるのがうざいので
      - source_labels: ['__journal__systemd_unit']
        target_label: 'unit'
        regex: '^(session).*(scope)$'
        action: replace
        replacement: session.scope

    pipeline_stages:
      - match:
          selector: '{unit="websh.service"}'
          stages:
            - regex:
                expression: |-
                  (?P<service>websh_[^\s]+)\s+\|\s+(?P<content>.*)$
            - json:
                expressions:
                  ip: xForwardedFor
                  time: time
                  level: level
                  code: code
                  msg: msg
                  elapsed_time: elapsedTime
                source: content
            - labels:
                service: service
                ip: ''
                time: ''
                level: ''
                code: ''
                msg: ''
                elapsed_time: ''

如果是从systemd-journal转移的日志,您可以使用source_labels字段并指定__journal__systemd_unit来进行筛选。
另外,由于systemd启动的服务名称会附加在单位标签中,所以在pipeline_stages的匹配选择器中也可以使用,非常方便。

由于需要通过docker-compose启动服务并捕获docker-compose的标准输出,因此docker-compose在日志输出时总是会添加前缀。

在此,我们使用promtail的正则表达式进行解析并进行了排除。
结果是,我们能够通过以下描述对日志进行标记,这样就轻松多了。

想法

我們正在積極進行應用程式和基礎設施的微調和維護,目前改進和運營都進行得順利,沒有太大的不便。

以前,Nim存在致命的错误,所以我们不得不从devel分支中获取最新的源代码,并在CI上构建编译器。但是,Nim 1.4.0解决了这些问题,现在我们的应用程序正常运行,没有任何问题。

我认为未来也会继续保守,但不知道会变成什么样。

现在我考虑的是把整个Web服务器用LXD虚拟化,并且希望能够进行快照,以便能够随时恢复。有时候由于基础设施的变更而出现问题,需要费劲地恢复到之前的状态,所以我想进行虚拟化,可以快速地进行切换和恢复。

如果这附近可以使用AWS,我可以通过拍摄实例快照等方式来处理,所以如果可以使用AWS的话,我会想要用AWS。

以上了。

广告
将在 10 秒后关闭
bannerAds