部署Common Lisp Web应用程序

17

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

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

但是,我有些困惑如何将应用程序部署到像AWS EC2这样的生产服务器上。我应该以什么形式部署Lisp代码?是否有不同的选项?如果服务器需要重新启动或出现问题会发生什么?

3个回答

25

最近,我通过构建自包含的Web应用程序可执行文件发现了一些东西,并在lisp-journey/web-dev(发布和部署部分)以及Common Lisp Cookbook/scripting#for-web-apps上写了一些内容。

这里复制了一些有趣的部分,每个资源上还有更多内容。欢迎编辑,主要感谢那些资源!

编辑 2019年7月:我在Cookbook上贡献了一个页面: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库。

如何构建自包含可执行文件

有关绑定到Homebrew和Debian软件包的信息,请参见https://github.com/CodyReichert/awesome-cl#interfaces-to-other-package-managers

使用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中运行此命令,则会收到有关运行线程的错误:

Cannot save core with multiple threads running.

从简单的SBCL repl中运行该命令。

我假设您的项目具有Quicklisp依赖项。然后,您必须:

  • 确保在Lisp启动时安装并加载Quicklisp(您已完成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)

通过命令行或Makefile,使用--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

因此,在Makefile中:

LISP ?= sbcl

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

使用Roswell或Buildapp

Roswell是一个实现管理器,还有ros build命令,可以适用于许多实现。

我们也可以使用ros install my-app在Roswell中安装我们的应用程序。请参阅其文档。

最后,我们提到Buildapp,它是一个经过测试并仍然广受欢迎的“为SBCL或CCL配置和保存可执行Common Lisp图像的应用程序”。

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

对于Web应用程序

我们可以类似地为我们的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)。

解析命令行参数

请参阅食谱here。TLDR; 使用uiop:command-line-arguments获取参数列表。要进行真正的解析,需要使用库。

部署

通过可执行文件很容易。Web应用程序立即对外可见。

在Heroku上

请参阅this buildpack

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

请查看如何在您的系统上执行此操作。
大多数GNU/Linux发行版现在都配备了Systemd。
示例search结果:

只需要编写一个配置文件,就可以轻松完成:

# /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

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

我们可以使用M-x slime-connect连接正在运行的Swank,输入端口4006。

我们可以编写新代码:

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

例如,使用M-x slime-eval-region等方式正常评估它。输出应该会更改。

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

热重载

使用Quickutil进行示例。请参阅lisp-journey上的注释。

它必须在服务器上运行(一个简单的fabfile命令可以通过ssh调用此命令)。事先,fab update已在服务器上运行了git pull,因此新代码存在但不在运行中。它连接到本地swank服务器,加载新代码,连续停止和启动应用程序。

持续集成、可执行文件的持续交付、Docker

请参阅https://lispcookbook.github.io/cl-cookbook/testing.html#continuous-integration


当您编写asdf:make :my-package时,我本来期望看到的是asdf:make :my-system,因为尽管系统和软件包通常被命名为相同的名称,但这并非总是如此,这可能会带来混淆。除此之外,回答很棒! - coredump
谢谢伙计!你的Lisp答案一如既往地棒极了!顺便问一下,你是深町英太郎吗?如果是的话,我想告诉你,你在Lisp社区的活动真的很令人印象深刻 :) - MadPhysicist
2
@coredump 很好的观点,谢谢!MadPhysicist:谢谢!哇,我很感激你把我当成了E. Fukamachi^^ 我希望我是,但事实上,我离他那么有天赋和高效还差得远呢!我是这两个页面的作者,你可以看到,我的Github昵称是vindarel。 - Ehvince
1
是的,这就是我的Lisp之旅,我在回答中做了不害臊的自我推销,但我很高兴它对新手有帮助:我记住了这一点,因为我也是。是的,我们需要在食谱中加入这样的章节,我正在收集笔记。这里没有太多空间。请参见malisper帖子,不要忘记(declaimbreak example和log4cl。请参见tracestepdefadvice等,在David B. Lamkins的“Successful lisp”中有一个大章节。在Cookbook中开一个问题! - Ehvince
@MadPhysicist,最好的解决方案似乎是(尚未尝试)Sly's stickers,正是您所要求的。 - Ehvince
显示剩余4条评论

6

要在生产中运行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)))"

2
这对于评估和编译没有实际区别,因为SBCL可以使用源文件并即时编译它。它减少了加载fasl文件的负载时间,因为代码已经编译过了。但是以这种方式调用SBCL是启动Lisp并执行一些代码的一种方式。 - Rainer Joswig
如果我以这种方式将应用程序投入生产,我是否能够远程连接并以所谓的“热交换”形式更改代码? - MadPhysicist
2
为此,最好在您的图像中启动一个 Swank 服务器。 - Svante

5

我找到了一篇博客,其中提供了一个解决方案,我已经根据自己的需要在Linux系统上进行了适应。不幸的是,我无法再找到那篇博客的参考资料,因此我不能向你展示原始解决方案,而我的解决方案是针对CCL的(而原始解决方案是针对SBCL的),因为我更熟悉CCL。以下是启动该系统的程序:

(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)

希望这能有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接