部署常见的 Lisp Web 应用程序



我想知道如何部署一个用Hunchentoot,Wookie,Woo甚至Clack编写的Common Lisp Web应用程序。

也就是说,假设我编写了一个包含一些文件、包等的应用程序。通常,当我在本地工作时,我只需在 REPL 中运行一个命令来启动服务器,然后使用localhost:8000或类似的东西访问它。

但是,我对将应用程序部署到 AWS EC2 等生产服务器的过程感到有些困惑。我应该以什么形式部署 Lisp 代码?有不同的选择吗?如果服务器需要重新启动或遇到问题,会发生什么情况?

我最近通过为 Web 应用程序构建独立的可执行文件来弄清楚一些事情,我在 lisp-journey/web-dev(发布和部署部分)以及 Common Lisp Cookbook/scripting#for-web-apps 上的构建部分写了关于它的文章。

我在这里复制有趣的部分,每个资源上都有更多内容。欢迎编辑,主要是在这些资源上,谢谢!

编辑2019 年 7 月:我在食谱上贡献了一个页面:https://lispcookbook.github.io/cl-cookbook/web.html

编辑:另请参阅提供专业CL支持的工具和平台列表:https://github.com/CodyReichert/awesome-cl#deployment

(已编辑)如何将 Web 应用程序作为脚本运行

我将在下面解释如何构建和运行可执行文件,但我们当然可以将应用程序作为脚本运行。在 lisp 文件中,比如说run.lisp,确保:

  • 要加载项目的 ASD 文件,请执行以下操作:(load "my-project.asd")
  • 加载其依赖项:(ql:quickload :my-project)
  • 调用其主函数:(my-project:start)(给定start是导出的符号,否则::start)。

在这样做的过程中,应用程序启动并返回一个 Lisp REPL。您可以与正在运行的应用程序进行交互。您可以更新它,甚至可以在运行时安装新的 Quicklisp 库。

如何构建独立的可执行文件

另请参阅 https://github.com/CodyReichert/awesome-cl#interfaces-to-other-package-managers 了解与 Homebrew 和 Debian 软件包的绑定。

与SBCL

如何构建(自包含)可执行文件是特定于实现的(请参阅 在Buildapp和Rowsell下面)。对于 SBCL,如前所述 其文档, 这是一个问题:

(sb-ext:save-lisp-and-die #P"path/name-of-executable" :toplevel #'my-app:main-function :executable t)

sb-ext是用于运行外部进程的 SBCL 扩展。 查看其他 SBCL 扩展 (其中许多在其他库中实现可移植)。

:executable t告诉构建可执行文件而不是 图像。我们可以构建一个图像来保存当前状态 Lisp 图像,稍后再使用它。在以下情况下特别有用 我们做了很多计算密集型工作。

如果您尝试在 Slime 中运行它,您将收到有关线程运行的错误:

无法在运行多个线程的情况下保存内核。

从简单的 SBCL 复制运行命令。

我想你的项目有Quicklisp依赖项。然后,您必须:

  • 确保 Quicklisp 在 Lisp 启动时安装并加载(您 完成 Quicklisp 安装)
  • load项目的 .asd
  • 安装依赖项
  • 生成可执行文件。

这给出了:

(load "my-app.asd")
(ql:quickload :my-app)
(sb-ext:save-lisp-and-die #p"my-app-binary" :toplevel #'my-app:main :executable t)

从命令行或生成文件,使用--load--eval

build:
sbcl --non-interactive 
--load my-app.asd 
--eval '(ql:quickload :my-app)' 
--eval "(sb-ext:save-lisp-and-die #p"my-app" :toplevel #my-app:main :executable t)"

使用ASDF

现在我们已经了解了基础知识,我们需要一种便携式方法。自其 版本3.1,ASDF允许这样做。它引入了make命令, 从 .asd 读取参数。将此添加到 .asd 声明中:

:build-operation "program-op" ;; leave as is
:build-pathname "<binary-name>"
:entry-point "<my-system:main-function>"

并打电话给asdf:make :my-system.

因此,在生成文件中:

LISP ?= sbcl
build:
$(LISP) --non-interactive 
--load my-app.asd 
--eval '(ql:quickload :my-app)' 
--eval '(asdf:make :my-system)' 

与罗斯威尔或构建应用程序

罗斯威尔,一个实施经理等等 更多,还具有ros build命令,应该适用于许多人 实现。

我们还可以使我们的应用程序可与罗斯威尔一起安装ros install my-app.请参阅其文档。

我们将以一句话结束 Buildapp,一个久经考验的和 仍然很受欢迎"配置和保存的 SBCL 或 CCL 应用程序 一个可执行的 Common Lisp 映像"。

许多应用程序使用它(例如, pgloader),它可在 Debian:apt install buildapp,但是你现在不应该需要asdf:make或Roswell。

对于网络应用

我们同样可以为我们的 Web 应用程序构建一个独立的可执行文件。它 因此将包含一个 Web 服务器,并且能够在 命令行:

$ ./my-web-app
Hunchentoot server is started.
Listening on localhost:9003.

请注意,这运行的是生产 Web 服务器,而不是开发服务器, 因此我们可以立即在VPS上运行二进制文件,并从以下位置访问该应用程序 外面。

我们有一件事要处理,那就是找到并放置线程 前台正在运行的 Web 服务器。在我们的main职能中,我们 可以做这样的事情:

(defun main ()
(start-app :port 9003) ;; our start-app, for example clack:clack-up
;; let the webserver run.
;; warning: hardcoded "hunchentoot".
(handler-case (bt:join-thread (find-if (lambda (th)
(search "hunchentoot" (bt:thread-name th)))
(bt:all-threads)))
;; Catch a user's C-c
(#+sbcl sb-sys:interactive-interrupt
#+ccl  ccl:interrupt-signal-condition
#+clisp system::simple-interrupt-condition
#+ecl ext:interactive-interrupt
#+allegro excl:interrupt-signal
() (progn
(format *error-output* "Aborting.~&")
(clack:stop *server*)
(uiop:quit)))
(error (c) (format t "Woops, an unknown error occured:~&~a~&" c))))

我们使用了bordeaux-threads库((ql:quickload "bordeaux-threads"),别名bt)和uiop,它是ASDF的一部分,所以已经加载,以便以可移植的方式退出(uiop:quit,带有 可选的返回代码,而不是sb-ext:quit)。

解析命令行参数

请参阅此处的食谱。TLDR;使用uiop:command-line-arguments获取参数列表。为了真正解析它们,有库。

部署

使用可执行文件很简单。Web 应用程序立即从外部可见。

在希罗库

请参阅此构建包。

守护进程,崩溃时重新启动,处理日志

了解如何在您的系统上执行此操作。

大多数GNU/Linux发行版现在都带有Systemd。

示例搜索结果:

  • https://seanmcgary.com/posts/deploying-nodejs-applications-with-systemd/

它就像编写配置文件一样简单:

# /etc/systemd/system/my-app.service
[Unit]
Description=stupid simple example
[Service]
WorkingDirectory=/path/to/your/app
ExecStart=/usr/local/bin/sthg sthg
Type=simple
Restart=always
RestartSec=10

运行命令以启动它:

sudo systemctl start my-app.service

用于检查其状态的命令:

systemctl status my-app.service

Systemd 可以处理日志记录(我们写入 stdout 或 stderr,它写入日志):

journalctl -f -u my-app.service

它处理崩溃并重新启动应用程序

Restart=always

它可以在重新启动后启动应用程序

[Install]
WantedBy=basic.target

要启用它,请执行以下操作:

sudo systemctl enable my-app.service

调试 SBCL 错误:ensure_space:无法分配 n 个字节

如果服务器上的 SBCL 出现此错误:

mmap: wanted 1040384 bytes at 0x20000000, actually mapped at 0x715fa2145000
ensure_space: failed to allocate 1040384 bytes at 0x20000000
(hint: Try "ulimit -a"; maybe you should increase memory limits.)

然后禁用 ASLR:

sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"

连接到远程 Swank 服务器

这里有个小例子:http://cvberry.com/tech_writings/howtos/remotely_modifying_a_running_program_using_swank.html。

演示项目在这里:https://lisp-journey.gitlab.io/blog/i-realized-that-to-live-reload-my-web-app-is-easy-and-convenient/

它定义了永久打印的简单函数:

;; a little common lisp swank demo
;; while this program is running, you can connect to it from another terminal or machine
;; and change the definition of doprint to print something else out!
;; (ql:quickload :swank)
;; (ql:quickload :bordeaux-threads)
(require :swank)
(require :bordeaux-threads)
(defparameter *counter* 0)
(defun dostuff ()
(format t "hello world ~a!~%" *counter*))
(defun runner ()
(bt:make-thread (lambda ()
(swank:create-server :port 4006)))
(format t "we are past go!~%")
(loop while t do
(sleep 5)
(dostuff)
(incf *counter*)
))
(runner)

在我们的服务器上,我们运行它

sbcl --load demo.lisp

我们在开发机器上进行端口转发:

ssh -L4006:127.0.0.1:4006 username@example.com

这会将服务器上的端口 4006 安全地转发到 example.com 我们本地计算机的端口 4006(Swanks 接受来自 本地主机)。

我们用M-x slime-connect连接到正在运行的swank,输入 端口 4006。

我们可以编写新代码:

(defun dostuff ()
(format t "goodbye world ~a!~%" *counter*))
(setf *counter* 0)

例如,像往常一样用M-x slime-eval-region评估它。输出应更改。

CV Berry的页面上有更多指针。

热重载

使用快速使用的示例。请参阅有关 lisp-journey 的注释。

它必须在服务器上运行(一个简单的fabfile命令可以调用这个 通过 SSH)。事先,fab updategit pull已在 服务器,因此存在新代码但未运行。它连接到 本地 Swank 服务器,加载新代码,停止和启动应用程序 排。

持续集成,持续交付可执行文件,Docker。

见 https://lispcookbook.github.io/cl-cookbook/testing.html#continuous-integration

要在生产环境中运行 Lisp 映像,你可以使用以下命令从 Lisp 代码生成一个 fasl 文件:

(compile-file "app.lisp")

通过调用 SBCL 运行生成的 .fas 文件。

sbcl --noinform 
--load app.fas 
--eval "(defun main (argv) (declare (ignore argv)) (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)))"

我找到了一个博客,其中包含一个解决方案,我已经适应了我对Linux机器上生产系统的需求。不幸的是,我再也找不到对该博客的引用,因此我可以向您展示我的解决方案,该解决方案适用于CCL(而原始解决方案适用于SBCL),我对此更熟悉。这是启动系统的程序:

(require 'swank)
(require 'hunchentoot)
(defparameter *httpd-port* 9090)     ; The port Hunchentoot will be listening on
(defparameter *shutdown-port* 6700)  ; The port CCL will be listening for shutdown
; this port is the same used in /etc/init.d/hunchentoot
(defparameter *swank-port* 5016)     ; The port used for remote interaction with slime
;; Start the Swank server
(defparameter *swank-server*
(swank:create-server :port *swank-port* :dont-close t))
(require 'YOUR-PACKAGE)
(YOUR-PACKAGE:YOUR-STARTING-FUNCTION)
(princ "Hunchentoot started on port ")
(princ *httpd-port*)
(terpri)
(let* ((socket (make-socket :connect :passive :local-host "127.0.0.1" :local-port *shutdown-port* :reuse-address t))
(stream (accept-connection socket)))
(close stream)
(close socket))
(print "Stopping Hunchentoot...")
(YOUR-PACKAGE:YOUR-STOPPING-FUNCTION)
(dolist (proc (all-processes))
(unless (equal proc *current-process*)
(process-kill proc)))
(sleep 1)
(quit)

这个想法是,您可以通过指定swank使用的端口来连接到带有slime的正在运行的系统。我用了几次,例如动态更改数据库链接,并且对这种可能性的力量印象深刻。

正在运行的系统可以通过以下方式终止:

telnet 127.0.0.1 6700

并由以下内容发起:

nohup ccl -l initcclserver.lisp >& server.out &

在以前版本的脚本中,我找到了特定于 SBCL 的部分,因此,如果您使用它,您可以修改脚本。

要接受终止连接:

(sb-bsd-sockets:socket-bind socket #(127 0 0 1) *shutdown-port*)
(sb-bsd-sockets:socket-listen socket 1)
(multiple-value-bind (client-socket addr port)
(sb-bsd-sockets:socket-accept socket)
(sb-bsd-sockets:socket-close client-socket)
(sb-bsd-sockets:socket-close socket)))

要关闭系统:

(dolist (thread (sb-thread:list-all-threads))
(unless (equal sb-thread:*current-thread* thread)
(sb-thread:terminate-thread thread)))
(sleep 1)
(sb-ext:quit)

希望这能有所帮助。

最新更新