开源 wiki 和知识管理系统 Outline 的快速部署脚本

项目地址: https://github.com/vicalloy/outline-docker-compose

前一段想部署一个 Wiki ,看了一圈被 Outline 的颜值吸引。Outline 支持部署到自己服务器,只是自部署的体验真的不太好。Outline 部署主要就下面几个问题。

  1. 默认使用 AWS 的 S3 服务。
  2. 不提供用户管理模块,需通过 Slack 、Google 或是自建 OIDC 服务进行登录。

网上已用基于 docker-compose 的部署方案主要有两个:

  1. outline-wiki-docker-compose
    1. 提供交互式脚本,生成 docker-compose 配置文件。
    2. 使用 Slack 进行登录,国内使用体验不好。
    3. 脚本太老,存在 Bug ,图片上传后显示不了。
  2. docker-outline
    1. 国内用户写的部署脚本。目前中文网络环境下搜索 Outline 找到的都是 soulteary 的文章。
    2. 脚本不够智能,配置参数还是有些多。
    3. 内置了 OIDC 服务,不过好像不支持用户管理,只能创建一个用户。(注:没仔细研究过,不确定)

出于自己部署的需要,参考 outline-wiki-docker-compose 的实现,新开了 Outline 的部署项目。

  1. 配置文件尽量简化。只有 config.sh 一个配置文件,其他配置文件由脚本生成。
  2. 内置 OIDC 服务,可以直接通过 Web 管理用户。

ODIC Server

项目地址:https://github.com/vicalloy/oidc-server/

多很多想自己部署 Outline 的人来说,没有本地的认证系统是一个非常头痛的问题。该Issue Local Authentication #1881 的评论数是 Outline 未关闭 Issue 中最多的。我也一度被该问题劝退。

可能 Outline 主推的还是 Cloud 版,虽然内建认证对自部署影响很大,官方依旧没有给出明确的支持方案。注:不少服务是提供OIDC认证的,如果你同时使用Gitlab,你可以使用Gitlab做OIDC认证服务器。

我最初的想法部署一个做简单的ODIC认证服务器给 Outline。出于节约内存的考虑,这个服务最好是Go或是Rust编写。找了一圈未发现合适的应用,于是回到了自己最熟悉的 Django 上。

OIDC Server 主体代码来源于 django-oidc-provider 的 example 。甚至可以说这个项目就是将 example 做了个打包。依托于 Django 优秀的插件机制和 Admin 模块,在几乎不用写代码的情况下就可以得到一个还过得去的 OIDC Server。当然缺点是内存占用量有点大,这个服务需要用掉大概100M的内存。

注:ODIC Server docker镜像使用 Github Action 进行构建。不得不说 Github Action 的体验真的非常棒。

把手机从洗衣机里抢救出来及后续

洗完澡后将手机和一堆的衣服丢进了洗衣机,等到发现的时候手机已经和衣服一起洗了20分钟。好在现在的手机防水效果做的还不错(iPhone 12mini),找到的时候手机还是亮的,除了手机上的钢化膜裂了外没有发现明显问题。

擦干后,发现下麦克风还会继续渗出水来,轻甩了几下持续出水。手机进水,网上的常规方法是将手机和大米(干燥机)放一起,让手机缓慢干燥。考虑到手机状态还可以(能正常使用),应当是手机的密封效果还不错。进去的水比较少,同时水比较难自行挥发出来,手机不知道要放多久才能用。再三考虑后决定用烤箱低温“烘培”。

将手机放入烤箱,开40°C,并开启热风循环。刚开始手机下听筒会渗出少许的水,过一段时间后外表就看不到明显的水了。在烘了1个小时后,怕高温影响电池寿命,将温度调低到38 °C ,并将烤箱定时调到6个小时(最多定时6小时),然后睡觉。

目前手机没有出现明显问题,后续如有新的情况再更新。

注:

  • 要用低温模式。如果你的烤箱不支持低温发酵,不要轻易尝试。
  • 别用微波炉。微波炉的原理和烤箱不一样,弄不好手机会炸。
  • 烘手机的时候最好把SIM卡取掉。
  • 网上看了一圈,好像没什么烤箱烘手机的先例(也没几个人把手机丢洗衣机里),不知道有没有什么潜在风险。

将服务器迁移到腾讯云

之前因为嫌备案麻烦,一直用的国外的主机。只是网站的访问速度一言难尽,毫无体验可言。

近期腾讯云做活动,1G内存3年只要¥150,简直和不要钱一样。禁不住诱惑上车了。

服务搬家

Docker

由于有了2G的“大内存”(之前只有1G内存),搬家后所有的服务都改用Docker部署。

目前服务器上跑的服务有:

  • nginx 网关,将各个子域名路由到对应服务上。
  • wordpress 我的个人博客。
    • 启用https后,css和js等静态资源始终请求的http地址,导致资源无法加载。折腾了很久都没有搞定,仔细分析后认为应当是nginx做了proxy后wordpress不知道已经换成了https,依旧生成http的资源访问地址。在nginx中增加配置 proxy_set_header X-Forwarded-Proto $scheme; 解决问题。
  • filebrowser 私人网盘。
    • 注:filebrowser,使用Go开发,部署起来比较简单。不过我不想配置systemd,为了开机自动启动功能继续使用Docker。
  • django-lb-workflow演示站点

注:可以在docker-compose.ym中将networks设置为external实现不同docker-compose之间的容器互联。由于我使用nginx作为网关,因此所有服务都使用nginxnetwork

version: "3"
services:
  filebrowser:
    image: filebrowser/filebrowser:latest
    restart: always
    ports:
      - ${IP}:10180:80
    volumes:
      - ./data:/srv
      - ./db/database.db:/database.db
    networks:
      - nginx_default
networks:
  nginx_default:
    external: true

HTTPS & DNS

之前一直使用 certbot 进行免费证书的申请,只是这东西的体验一直不是很好。这次换成了acme.shacme.sh 完全使用shell脚本编写,使用起来非常简单,按照官网文档很快就可以弄好。

之前为了改善网站的访问速度使用了 cloudflare 的CDN功能(然而速度一如既往的慢),域名服务也一并迁到了 cloudflare 。既然不再使用 cloudflare DNS的解析也迁回了国内的 DNSPOD

注:免费的HTTPS证书已经支持泛域名了,泛域名只支持DNS方式进行认证。

总结

  • 访问速度提升巨大,体验好了很多。之前连SSH都容易卡掉线。
  • 之前跑在服务器上 telegram-shell-bot 连不上服务器了,被迫停工。
  • 网站备案比预期的要简单些。备案审核需要1~2周时间,期间网站访问不了。

关于kkndme谈房产

不知道为啥,kkndme的帖子忽然出现在GitHub的热榜上,而且被冠以神贴的称号。简单的看了一下缩水版的帖子。帖子有些意思,但要说是神帖似乎有些过了。

总结

别指望房子降价,房子是中国割韭菜的主要工具,没有找到替代手段前国家不会放弃的(暂时也找不到)。别老想着买房投资。房子是给国家赚钱的,你们这些二手房就别来添乱了。

看法

kkndme文中公知味很足,文中的内容要辩证的看。11年后房价确实涨过一波,不过并不是一直在涨。10年过去了,国内外形势已经有很大的不同,房子还能不能涨还真不一定(也别指望跌)。房价重要的是要维持在一个“合理的范围内”。多少钱合理,主要看能掏钱买房的“刚需”有多少。别被打着kkndme旗号的公众号收智商税。

在线使用的图片风格迁移工具

近期研究 ONNX Runtime Web 做的一个小东西。很多代码都“借鉴”于其他开源项目,解决了图片变形等问题。

使用深度学习模型做的图片风格迁移。使用 React 和 ONNX Runtime Web 开发,推理后端用的 WebAssembly ( CPU )。根据我的测试,用 WebGL 要慢不少,而且内存占用有些夸张。

在线访问: https://vicalloy.github.io/image-transformer/

项目地址: https://github.com/vicalloy/image-transformer

Note:

  1. 所有推理工作在浏览器完成,不需要消耗服务器资源。
  2. 推理的时候需要选择图片的输出大小。不同大小的输出用的模型不同。
    • 越大的输出尺寸需要耗费的算力越多(时间越长),测试的时候可以先用小尺寸看效果。
    • fast neural style这个模型也可以支持输出任意图片大小,不过动态参数模型太大,复杂度也高,不适合 Web 使用。

新疆之行

本计划去年去新疆,不想因为疫情的关系错过了,今年算是把去年的计划做了个补完。由于错误的低估了新疆入冬的速度,未能走成独库公路。这是一次不完美,甚至是失败的旅行,不过依旧留下了一些美好。

赛里木湖

9月底的新疆早已入秋,草早已黄了,湖边的景致早已不如盛夏。湖很完美,比青海湖要漂亮。风很大,湖很蓝。浪花拍打着湖岸,然人觉赛里木湖更像是大海。

如果有机会再来赛湖,会选择在湖边住一晚。在湖边发发呆还是很惬意的。

夏塔

草地以及不远处的雪山,是我喜欢的画面。只是现在已经过了夏塔最漂亮的季节。出发前就一直很纠结要不要去夏塔。

由于规划不周,第一天车开到晚上11点才找到住处。第二天选择了去距离比较近的那拉提草原。

那拉提

也许是季节的原由,那拉提并没有给我很惊艳的感觉。独库路上风景要比那拉提漂亮。

巴音布鲁克

一望无际的大草原。如果夏季过来,应当会是遍地的野花吧。

独库公路

新疆之行最大的期待就是独库公路,不想独库公路瞬间入冬。独库封路,在山下等了一天,第二天依旧没有任何解封的迹象,只能绕道赛湖回乌鲁木齐。

第一天从上午11点一直等到下午5点,不慎将汽车电瓶耗尽,无法启动。非常感谢检查站的小哥帮忙找了个车接电,不然荒郊野外的还不知道修车厂来不来。

虽然只走了那拉提到巴音布鲁克这一小段独库公路,不过已经可以一窥独库公路的美了。

家里的小可爱

或许这次旅行并不能留下太多的记忆,但希望能成为他成长过程中美好经历的一部分。

2021年Python工具链

1. Python虚拟环境:Poetry

一个类似Pipenv的Python虚拟环境和依赖管理的工具,据称改善了一些Pipenv的问题。对我而言,主要看重了Poetry可以对Python库打包的功能。毕竟对我而言书写 setup.py 并不是一件很让人愉快的事情。

2. 代码静态扫描:Flake8

Flake8使用起来非常简单,不用这么配置就可以直接使用,之后检查过程中遇到自己不需要的规则,加个例外就好。

Flake8支持插件,通过添加插件还可以让Flake8功能变的更为强大。

[2023-02 update]: 我的代码检查工具切换成了 ruff 。速度要快很多,且同样易用。

3. 代码自动格式化:Black

写代码时,我个人会尽量遵守 PEP8 ,但难保团队中有些人代码写的有些随意。为保证编码风格的统一,在代码提交前统一由Black对代码镜像格式化。自动格式化之后的代码可能会少了那么一点个性,但为了统一还是值得的。

4. Import规则检查&格式化工具:isort

Black不会对Python 的 import 语句进行排序和分段,这个工作就交给isort来做了。

5. 类型检查:Mypy

长久以来Python作为脚本语言,程序里没有类型信息,很多本可在编译阶段发现的问题被保留到运行时。Python在3.5之后开始支持 Type Hint 了。利用Mypy可以利用这些类型信息对程序进行校验。

6. 单元测试:pytest

相比 unittest ,pytest使用上更为方便。更为重要的是pytest兼容 unittest,似乎没有什么理由来拒绝pytest。 

7. 测试覆盖率:Coverage.py

代码覆盖率测试工具好像也没有第二个选择。

8. pre-commit

git commit 时调用flake8进行代码检查,调用black对代码进行格式化等操作。利用pre-commit从源头上杜绝有人把不合格的代码提交到代码库。

9. Docker、Gitlab-CI、GitHub Action、Travis CI

CI服务可根据自己的实际情况进行选择

将OpenVINO预训练模型转为为ONNX,并使用TVM进行优化

OpenVINO是Intel推出的一款深度学习工具套件。OpenVINO带来大量的预训练模型,使用这些预训练模型可以快速的开发出自己的AI应用。

不过既然是Intel出的东西,自然少不了和Intel平台深度绑定。OpenVINO主要针对Intel的CPU进行优化。虽然也可以支持GPU,但支持的是Intel家的GPU。Intel家的GPU,应当不用报太多期待了。

为了支持更丰富的硬件类型,可以将OpenVINO自带的预训练模型 转为ONNX格式,然后在做其他处理。

OpenVINO模型导出为ONNX

OpenVINO优化后的预训练模型无法直接转换为ONNX。不过好在Intel有提供模型的训练和导出工具,利用OpenVINO的训练工具导出ONNX

OpenVINO用于训练和导出的库为: https://github.com/openvinotoolkit/training_extensions

具体的操作方式参见项目的具体说明文档。

对照人脸检测的文档,导出人脸检测对应ONNX模型: https://github.com/openvinotoolkit/training_extensions/tree/develop/models/object_detection/model_templates/face-detection

注:导出目录里有 export/export/alt_ssd_export/ 两种模型。其中 export/alt_ssd_export/ 包含了OpenVINO特有的实现,在转换为其他推理引擎模型时会失败,因此后续工作使用 export/ 中的模型。

使用TVM对ONNX模型进行优化

针对TVM的VM进行优化

对于存在动态shape的模型,TVM无法进行编译。很不幸的是OpenVINO中物体检测相关的模型都存在动态shape。在TVM无法编译的情况下,可使用TVM的VM进行执行。

  • 注:
    • 关于VM的相关内容请阅读: https://tvm.apache.org/docs/dev/virtual_machine.html
    • TVM的文档比较欠缺(特别是VM相关的内容)。不过好在项目还在快速迭代过程中,提交的issue很快就可以得到回复。
    • 根据测试,使用VM模式,在CPU上TVM的速度甚至比用 ONNXRuntime 还要慢不少。不知道是否是跑在虚拟机上的关系。
import onnx
import time
import tvm
import numpy as np
import tvm.relay as relay
target = 'llvm -mcpu=skylake'
model_path = 'face-detection-0200.onnx'
onnx_model = onnx.load(model_path)
shape = [1,3,256,256]
input_name = "image"
shape_dict = {
        input_name: shape,
        }
mod, params = relay.frontend.from_onnx(onnx_model, shape_dict)
print(relay.transform.DynamicToStatic()(mod))
with tvm.transform.PassContext(opt_level=3):
    executable = relay.vm.compile(mod, target="llvm", target_host=None, params=params)
code, lib = executable.save()
with open("code.ro", "wb") as fo:
    fo.write(code)
lib.export_library("lib.so")

针对TVM进行编译和优化

如果你的模型可以正常编译,那就没必要采用VM模式了。直接编译理论上优化效果要好很多。这里采用的是TVM范例中给出的图片分类模型。

一个完整的模型优化和执行可以参考官方文档:Compiling and Optimizing a Model with the Python AutoScheduler

import onnx
import time
import tvm
import numpy as np
import tvm.relay as relay
target = 'llvm'
model_name = 'mobilenetv2'
model_path = f'{model_name}.onnx'
onnx_model = onnx.load(model_path)
mod, params = relay.frontend.from_onnx(onnx_model)
with relay.build_config(opt_level=3):
    graph, lib, params = relay.build(mod, target, params=params)
path_lib = f"./{model_name}.so"
lib.export_library(path_lib)
fo=open(f"./{model_name}.json","w")
fo.write(graph)
fo.close()
fo=open("./{model_name}.params","wb")
fo.write(relay.save_param_dict(params))
fo.close()

VM模式下加载和运行优化好的模型

加载前面导出的模型,并执行。


import onnx
import time
import tvm
import numpy as np
import tvm.relay as relay
def vmobj_to_array(o, dtype=np.float32):
    if isinstance(o, tvm.nd.NDArray):
        return [o.asnumpy()]
    elif isinstance(o, tvm.runtime.container.ADT):
        result = []
        for f in o:
            result.extend(vmobj_to_array(f, dtype))
        return result
    else:
        raise RuntimeError("Unknown object type: %s" % type(o))
shape = [1, 3, 224, 224]
model_path = 'face-detection-0200'
loaded_lib = tvm.runtime.load_module(f"{model_path}.tvm.so")
loaded_code = bytearray(open(f"{model_path}.tvm.code", "rb").read())
exe = tvm.runtime.vm.Executable.load_exec(loaded_code, loaded_lib)
ctx = tvm.cpu()
vm = tvm.runtime.vm.VirtualMachine(exe, ctx)
data = np.random.uniform(size=shape).astype("float32")
out = vm.run(data)
out = vmobj_to_array(out)
print(out)

Python多进程环境下日志模块导致死锁

近期公司的一个Python程序在启动新进程的时候总是会失败。在进程里可以看到对应的进程已经创建成功,但对应代码并未执行,且没有输入任何日志。

通过定位,发现问题源自Python的logging模块,在写文件模式下,logging模块是不支持多进程的。

问题产生原因

Python默认采用Fork方式创建新进程,在Fork新进程的时候会连同 也一同复制到新进程。

  1. 当主进程里有两个线程T1/T2,以及一个锁Lock1。
  2. 线程T2获取了锁Lock1,此时线程T1创建了一个新进程P2,此时Lock1被一同frok给了P2。
  3. P2执行时尝试等待Lock1解锁。由于线程T2不会被复制到P2,没有人给P2线程的Lock1解锁,导致P2死锁。

Python的logging模块在写文件时会加锁,由于锁被复制导致进程死锁。

注:由于创建新进程时锁会被复制,混用多进程和多线程时的加锁操作应当格外小心。

解决方案

根据Python的官方文档,logging模块不支持多进程模式下将日志保存到单一日志文件。多进程模式下日志保存方案,建议参考Python官方文档 Logging to a single file from multiple processes

参考资料: