我用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。
以上了。