将Lift应用程序部署到生产意味着比打包它更多,并确保您将运行模式设置为production
。本章中的配方显示了如何为各种托管服务做这些。
您还可以在自己的服务器上安装和运行Tomcat或Jetty等容器。在“运行您的应用程序”中引入了容器。这带来了需要了解如何安装,配置,启动,停止和管理每个容器,以及如何将其与负载平衡器或其他前端集成。这些是主题,您可以从这些来源中找到更多的内容:
- Lift维基 的部署部分。
- 托马斯·佩雷特,提升行动,第15章“部署和缩放”,曼宁出版社
- Jason Brittain和Ian F. Darwin,Tomcat:The Definitive Guide,O'Reilly Media,Inc.
- Tanuj Khare,Apache Tomcat 7 Essentials,Packt Publishing。
Lift系列包括与Lift相关的Tomcat配置选项页面。
部署到CloudBees
解
使用SBT package
命令生成可以部署到CloudBees的WAR文件,然后使用CloudBees SDK来配置和部署应用程序。
从CloudBees“Grand Central”控制台中,在您的帐户下创建一个新的应用程序。接下来,我们假设您的帐户被调用myaccount
,您的应用程序被调用myapp
。
为获得最佳性能,您需要确保Lift运行模式设置为“production”。从CloudBees SDK命令行执行此操作:
$
bees config:set -a myaccount / myapp run.mode=
production
这将将您的CloudBees应用程序的运行模式设置为生产myaccount/myapp
。省略-a
它将为您的整个CloudBees帐户设置它。
CloudBees将记住此设置,因此您只需要执行一次。
然后可以部署:
$ sbt package ... [info] Packaging /Users/richard/myapp/target/scala-2.9.1/myapp.war... ... $ bees app:deploy -a myaccount/myapp ./target/scala-2.9.1/myapp.war$
sbt包 ...[
info]
包装/Users/richard/myapp/target/scala-2.9.1/myapp.war ... ...$
蜜蜂app:deploy -a myaccount / myapp ./target/scala-2.9.1/myapp.war
这将会将您的WAR文件发送到CloudBees并进行部署。在bees app:deploy
命令完成后,您将看到应用程序输出的位置(URL)。
如果更改配置设置,则需要重新启动应用程序才能使设置生效。部署应用程序将执行此操作,否则运行bees app:restart
命令:
$
bees app:restart -a myaccount/myapp
讨论
如果要将应用程序部署到多个CloudBees实例,请注意,默认情况下,CloudBees将向每个实例轮询请求。如果您使用Lift的任何状态功能,则需要启用会话关联(粘性会话):
$ bees app:update -a myaccount/myapp stickySession=true
如果您使用的是Comet,它可以正常工作,但是CloudBees默认是启用 请求缓冲。这允许CloudBees执行智能操作,例如,如果一台机器没有响应,则重新路由集群中的请求。请求缓冲的结果是长时间的彗星请求会更频繁地超时。要关闭此功能,请运行以下操作:
$
bees app:update -a myaccount / myappdisableProxyBuffering
=
true
与运行模式设置一样,CloudBees将记住这些设置,因此您只需要设置一次。
最后,您可能希望增加JVM 的永久性生成内存设置。默认情况下,应用程序为PermGen分配64 MB。要将其增加到128 MB,请运行bees app:update
命令:
$
bees app:update -a myaccount / myappjvmPermSize
=
128
命令bees app:info
和bees config:list
将您的应用程序报到的设置。
RDBMS配置
如果您在应用程序中使用SQL数据库,则需要配置src / main / webapp / WEB-INF / cloudbees-web.xml。例如:
<?xml version="1.0"?>
<cloudbees-web-app
xmlns=
"http://www.cloudbees.com/xml/webapp/1"
>
<appid>
myaccount / myapp</appid>
<resource
name=
"jdbc/mydb"
auth=
"Container"
type=
"javax.sql.DataSource"
>
<param
name=
"username"
value=
"dbuser"
/>
<param
name=
"password"
value=
"dbpassword"
/>
<param
name=
"url"
value=
"jdbc:cloudbees://mydb"
/>
<!-- For these connections settings, see:
http://commons.apache.org/dbcp/configuration.html
-->
<param
name=
"maxActive"
value=
"10"
/>
<param
name=
"maxIdle"
value=
"2"
/>
<param
name=
"maxWait"
value=
"15000"
/>
<param
name=
"removeAbandoned"
value=
"true"
/>
<param
name=
"removeAbandonedTimeout"
value=
"300"
/>
<param
name=
"logAbandoned"
value=
"true"
/>
<!-- Avoid idle timeouts -->
<param
name=
"validationQuery"
value=
"SELECT 1"
/>
<param
name=
"testOnBorrow"
value=
"true"
/>
</resource>
</cloudbees-web-app>
这是一个JNDI数据库配置,定义了一个名为CloudBees数据库的连接mydb
。如果JNDI名称将由Lift使用在Boot.scala中引用:
DefaultConnectionIdentifier
.
jndiName
=
"jdbc/mydb"
if
(!
DB
.
jndiJdbcConnAvailable_?
)
{
// set up alternative local database connection here
}
因为JNDI设置仅在cloudbees-web.xml中定义,所以只能在CloudBees环境中使用。这意味着您可以在本地开发不同的数据库,并在部署时使用您的CloudBees数据库。
主机IP和端口号
通常,您不需要知道部署的实例的公共主机名和端口号。对您的应用程序URL的请求将由CloudBees路由到特定实例。但是有些情况,特别是当你有多个实例时,你需要找到这些实例。例如,如果要从亚马逊的简单通知服务(SNS)接收邮件,则每个实例都需要在应用程序引导时为SNS提供一个直接的URL。
CloudBees提供了有关如何执行此操作的文档。要获取公共主机名,您需要向http:// instance-data / latest / meta-data / public-hostname发出HTTP请求。例如:
import
io.Source
val
beesPublicHostname
:
Box
[
String
]
=
tryo
{
Source
.
fromURL
(
"http://instance-data/latest/meta-data/public-hostname"
).
getLines
().
toStream
.
head
}
这将Full
在CloudBees环境中返回主机名,但在本地运行时将失败并返回Failure
。例如:
Failure
(
instance
-
data
,
Full
(
java
.
net
.
UnknownHostException
:
instance-data
),
Empty
)
可以从应用程序部署的.genapps / ports文件夹中的文件的名称中找到端口号:
val
beesPort
:
Option
[
Int
]
=
{
val
portsDir
=
new
File
(
System
.
getenv
(
"PWD"
),
".genapp/ports"
)
for
{
files
<-
Option
(
portsDir
.
list
)
port
<-
files
.
flatMap
(
asInt
).
headOption
}
yield
port
}
该java.io.File list
方法返回目录中的文件名列表,但是null
如果该目录不存在或者有任何IO错误,则会返回。因此,我们将其包装Option
成将null
值转换为None
。
在本地运行,这将返回一个None
,但在CloudBees上,您会看到一个Full[Int]
端口号。
您可以将以下两个值放在一起:
import
java.net.InetAddress
val
hostAndPort
:
String
=
(
beesPublicHostname
openOr
InetAddress
.
getLocalHost
.
getHostAddress
)
+
":"
+
(
beesPort
getOrElse
8080
).
toString
在本地运行,hostAndPort
可能会192.168.1.60:8080
在CloudBees上运行,就像这样ec2-204-236-222-252.compute-1.amazonaws.com:8520
。
Java版本
目前,CloudBees提供的默认JVM是JDK 7,但您可以选择6,7和8.要更改默认Java虚拟机,请使用以下bees config:set
命令:
$
bees config:set -a myaccount / myapp -Rjava_version=
1.8
-a myaccount/myapp
从命令中排除应用程序标识符将将JVM设置为帐户中所有应用程序的默认值。该 bees config:set
命令将更新配置,但在应用程序更新或重新启动之前不会生效。
当通过以下命令部署或更新应用程序时,也可以更改JVM:
$
bees app:deploy -a myaccount / myapp sample.war -Rjava_version=
1.6$
bees app:update -a myaccount / myapp -Rjava_version=
1.7
要确认应用程序当前运行的JVM,请使用bees config:list
将显示Java版本的 命令:
$
bees config:list -a myaccount / myapp Runtime Parameters:java_version
=
1.6
集装箱版
CloudBees提供了几个容器:Tomcat 6.0.32(默认),Tomcat 7,JBoss 7.02,JBoss 7.1和GlassFish 3。
要更改容器,应用程序将需要重新部署,因为CloudBees为各种容器使用不同的文件配置。 因此我们使用bees app:deploy
命令。以下示例更新到Tomcat 7:
$
bees app:deploy -t tomcat7 -a myaccount / myapp sample.war
JVM和容器命令可以单独运行bees app:deploy
,如下所示:
$
bees app:deploy -t tomcat -a myaccount / myapp sample.war -Rjava_version=
1.6
这将使用Tomcat 6.0.32和JDK 6 将sample.war部署到应用myapp
程序myaccount
。
要确定应用程序部署到哪个容器,请使用以下命令bees app:info
:
$ bees app:info -a myaccount/myapp Application : myaccount/myapp Title : myapp Created : Wed Mar 20 11:02:40 EST 2013 Status : active URL : myapp.myaccount.cloudbees.net clusterSize : 1 container : java_free containerType : tomcat idleTimeout : 21600 maxMemory : 256 proxyBuffering : false securityMode : PUBLIC serverPool : stax-global (Stax Global Pool)
ClickStart
ClickStart应用程序是快速获取应用程序的模板,并在CloudBees上自动构建和运行。Lift ClickStart在CloudBees上创建一个包含Lift 2.4应用程序的私有Git源存储库,提供MySQL数据库,创建基于Maven的Jenkins构建,并部署应用程序。所有您需要做的是为应用程序提供一个名称(不含空格)。
要访问为您创建的Git源存储库,您需要上传SSH公钥。您可以在CloudBees网站上的帐户设置的“我的密钥”部分中执行此操作。
为您创建的构建将在将更改推送到Git存储库时自动构建并将应用程序部署到CloudBees。
如果所有这些都与您想要使用的技术和服务相匹配,ClickStart是部署应用程序的好方法。或者,它为您提供了一个起点,您可以从中修改元素; 或者您可以分配CloudBees Lift模板并创建自己的模板。
也可以看看
CloudBees SDK提供了配置和控制应用程序的命令行工具。
CloudBees开发人员门户网站包含一个“资源”部分,提供CloudBees服务的详细信息。在其中,您将找到有关PermGen设置,JVM选择和servlet容器的详细信息。
部署到亚马逊弹性豆串
解
创建一个新的Tomcat 7 环境,使用SBT将Lift应用程序打包成WAR文件,然后将应用程序部署到您的环境中。
要创建新的环境,请访问AWS控制台,导航到Elastic Beanstalk,并选择“Apache Tomcat 7”作为您的环境。这将创建并启动默认的Beanstalk应用程序。这可能需要几分钟,但最终会报告“成功运行版本示例应用程序”。您将显示应用程序的URL(类似于http://default-environment-nsdmixm7ja.elasticbeanstalk.com),访问您所提供的URL将显示正在运行的默认Amazon应用程序。
通过运行以下命令来准备WAR文件:
$
sbt package
这将写入一个WAR文件到目标文件夹。要从AWS Beanstalk Web控制台部署此WAR文件(参见图10-1),请选择“弹性Beanstalk应用程序详细信息”下的“版本”选项卡,然后单击“上传新版本”按钮。您将获得一个对话框,您可以在其中给出版本标签,并使用“选择文件”按钮选择刚构建的WAR文件。您可以一步上传和部署,或先上传,然后在控制台中选择版本,然后点击“部署”按钮。
Beanstalk控制台将显示“环境更新...”,几分钟后,它将报告“成功运行”。您的Lift应用程序现在在Beanstalk上部署并运行。
最后一步是启用Lift的生产运行模式。从AWS Beanstalk Web控制台的环境中,按照“编辑配置”链接。将出现一个对话框,在“容器”选项卡下,添加-Drun.mode=production
到“JVM命令行选项”,然后点击“应用更改”重新部署应用程序。
讨论
弹性Beanstalk提供了一个预构建的软件和基础设施堆栈,在这种情况下:Linux,Tomcat 7,64位“t1.micro”EC2实例,负载平衡和S3存储区。这是环境,它具有合理的默认设置。Beanstalk还提供了一种轻松部署Lift应用程序的方法。正如我们在这个配方中所看到的,你将应用程序(WAR文件)上传到Beanstalk并将其部署到环境中。
与许多云提供商一样,请记住,您希望避免本地文件存储。这样做的原因是允许在没有数据丢失的情况下终止或重新启动实例。使用Beanstalk应用程序,您确实有一个文件系统,您可以写入它,但如果图像重新启动,它将丢失。您可以获取持久的本地文件存储,例如使用Amazon弹性块存储但是你正在与平台的性质进行斗争。
日志文件被写入本地文件系统。要访问它们,请从AWS控制台导航到您的环境,进入“日志”选项卡,然后点击“快照”按钮。这将需要一个日志的副本并将它们存储在S3桶中,并给你一个链接到文件内容。这是一个显示各种日志文件内容的单个文件,而catalina.out将显示您的Lift应用程序的任何输出。如果要尝试保留这些日志文件,您可以将环境配置为每小时将“日志”从“容器”选项卡的“编辑配置”下移动到S3。
Lift应用程序WAR文件存储在存储日志的同一S3桶中。从AWS控制台,您可以在S3页面下找到名称为“elastbeanstalk-us-east-1-5989673916964”的S3页面。您会注意到,通过为每个文件名添加前缀,AWS上传使您的WAR文件名唯一。如果您需要能够在S3中说明这些文件之间的区别,那么一个很好的方法是version
在build.sbt文件中触发该值。此版本号包含在WAR 文件名中。
多个实例
Beanstalks 默认启用自动缩放。也就是说,它启动了Lift应用程序的一个实例,但是如果负载增加到阈值以上,最多可能有四个实例正在运行。
如果您正在使用Lift的状态功能,则需要从环境配置的“负载平衡器”选项卡启用粘性会话。它是一个复选框,名为“启用会话粘性” - 很容易错过,但是如果您第一次看不到,该标签将滚动以显示更多选项。
使用数据库
没有什么不寻常的,你必须使用Lift和Beanstalk的数据库。但是,Beanstalk确实能够让您轻松使用Amazon的关系数据库服务(RDS)。在创建Beanstalk环境或稍后从配置选项中,您可以添加一个可以是Oracle,SQL Server或MySQL数据库的RDS实例。
MySQL选项将创建一个MySQL InnoDB数据库。数据库可以从Beanstalk访问,但不能从互联网上的其他地方访问。要更改它,请从AWS Web控制台修改RDS实例的安全组。例如,您可以允许从您的IP地址访问。
使用关联的RDS实例启动应用程序时,JVM系统属性包括数据库名称,主机,端口,用户和密码的设置。你可以将它们一起拉到Boot.scala中:
Class
.
forName
(
"com.mysql.jdbc.Driver"
)
val
connection
=
for
{
host
<-
Box
!!
System
.
getProperty
(
"RDS_HOSTNAME"
)
port
<-
Box
!!
System
.
getProperty
(
"RDS_PORT"
)
db
<-
Box
!!
System
.
getProperty
(
"RDS_DB_NAME"
)
user
<-
Box
!!
System
.
getProperty
(
"RDS_USERNAME"
)
pass
<-
Box
!!
System
.
getProperty
(
"RDS_PASSWORD"
)
}
yield
DriverManager
.
getConnection
(
"jdbc:mysql://%s:%s/%s"
format
(
host
,
port
,
db
),
user
,
pass
)
那会给你一个例子Box[Connection]
,如果Full
你可以在一个SquerylRecord.initWithSquerylSession
电话中使用(见第7章)。
或者,您可能希望通过向所有值提供默认值来保证连接:
Class
.
forName
(
"com.mysql.jdbc.Driver"
)
val
connection
=
{
val
host
=
System
.
getProperty
(
"RDS_HOSTNAME"
,
"localhost"
)
val
port
=
System
.
getProperty
(
"RDS_PORT"
,
"3306"
)
val
db
=
System
.
getProperty
(
"RDS_DB_NAME"
,
"db"
)
val
user
=
System
.
getProperty
(
"RDS_USERNAME"
,
"sa"
)
val
pass
=
System
.
getProperty
(
"RDS_PASSWORD"
,
""
)
DriverManager
.
getConnection
(
"jdbc:mysql://%s:%s/%s"
format
(
host
,
port
,
db
),
user
,
pass
)
}
也可以看看
亚马逊提供了截图的演示,展示了如何创建Beanstalk应用程序。
Elastic Beanstalk,van Vliet et al。(O'Reilly)介绍了Beanstalk基础架构的细节,如何使用Eclipse,实现持续集成,以及如何破解实例(例如,使用Nginx作为Beanstalk的前端)。
“使用AWS弹性Beanstalk配置数据库”的Amazon文档更详细地描述了RDS设置。
部署到Heroku
解
将Lift应用程序打包为WAR文件,并使用Heroku deploy插件来发送和运行应用程序。这将为您提供在Tomcat 7下运行的应用程序。任何人都可以使用此方法来部署应用程序,但Heroku仅为Enterprise Java客户提供支持。
该食谱分三个步骤完成:一次性设置; 部署WAR; 以及您的Lift应用程序的配置以进行生产性能。
如果还没有这样做,请下载并安装Heroku命令行工具(“Toolbelt”)并使用您的Heroku凭据登录并上传SSH密钥:
$ heroku login Enter your Heroku credentials. Email: you@example.org Password (typing will be hidden): Found the following SSH public keys: 1) github.pub 2) id_rsa.pub Which would you like to use with your Heroku account? 2 Uploading SSH public key ~/.ssh/id_rsa.pub... done Authentication successful.
安装部署插件:
$ heroku plugins:install https://github.com/heroku/heroku-deploy Installing heroku-deploy... done
通过一次性设置完成,您可以在Heroku上创建一个应用程序。这里我们没有指定一个名字,所以我们将给出一个随机的名称“glacial-waters-6292”,我们将在整个食谱中使用:
$ heroku create Creating glacial-waters-6292... done, stack is cedar http://glacial-waters-6292.herokuapp.com/ | git@heroku.com:glacial-waters-6292.git
在部署之前,我们将Lift运行模式设置为生产。这是通过config:set
命令完成的。首先检查当前设置JAVA_OPTS
,然后通过添加修改选项-Drun.mode=production
:
$ heroku config:get JAVA_OPTS --app glacial-waters-6292 -Xmx384m -Xss512k -XX:+ UseCompressedOops $ heroku config:set JAVA_OPTS =“ - Drun.mode = production -Xmx384m -Xss512k -XX:+ UseCompressedOops“--app glacial-waters-6292
我们可以通过将应用程序打包为WAR文件,然后运行Heroku deploy:war
命令来部署到Heroku :
$ sbt package .... [info] Packaging target/scala-2.9.1/myapp-0.0.1.war ... .... $ heroku deploy:war --war target/scala-2.9.1/myapp-0.0.1.war --app glacial-waters-6292 Uploading target/scala-2.9.1/myapp-0.0.1.war............done Deploying to glacial-waters-6292.........done Created release v6
您的Lift应用程序现在在Heroku上运行。
讨论
有关HerokuLift应用的几个重要评论。首先,请注意,不支持会话关联。如果部署到多个这就意味着DYNOS(Heroku的术语为实例),也没有协调过该请求到哪个服务器。因此,您将无法使用Lift的状态功能,并希望将其关闭(“运行无状态”描述如何执行此操作)。
其次,如果您使用的是LiftComet功能,则在Boot.scala中进行调整,以便在Heroku环境中更好地工作:
LiftRules
.
cometRequestTimeout
=
Full
(
25
)
此设置控制Lift在测试彗星连接之前等待的时间。由于Heroku在30秒后终止连接,所以我们正在将Lift的默认值替换为120秒,持续25秒。虽然Lift从此恢复,用户体验可能是在与页面交互时看到延迟。
要注意的第三个重要的一点是,每天都会重新启动该动力。另外,如果你只运行一个web dyno,它将在一个小时的闲置后空闲。您可以通过拖延应用程序日志来看到这种情况:
$ heroku logs -t --app glacial-waters-6292 ... 2012-12-31T11:31:39+00:00 heroku[web.1]: Idling 2012-12-31T11:31:41+00:00 heroku[web.1]: Stopping all processes with SIGTERM 2012-12-31T11:31:43+00:00 heroku[web.1]: Process exited with status 143 2012-12-31T11:31:43+00:00 heroku[web.1]: State changed from up to down
任何访问Lift应用程序的人都将导致Heroku联合您的应用程序。
请注意,应用程序已停止SIGTERM
。这是一个发送到一个进程的Unix信号,在这种情况下JVM要求它停止。不幸的是,Heroku上的Tomcat应用程序没有使用此信号来请求Lift关闭。这可能对您没有什么影响,但是如果您有外部资源要发布到关闭时执行的其他操作,则需要注册JVM的关闭钩子。
例如,如果您在Heroku上运行,则可以将其添加到Boot.scala中:
Runtime
.
getRuntime
().
addShutdownHook
(
new
Thread
{
override
def
run
()
{
println
(
"Shutdown hook being called"
)
// Do useful clean up here
}
})
不要指望在关机时能够做很多事情。Heroku允许大约10秒钟后,发出JVM之后才能杀死JVM SIGTERM
。
可能更通用的方法是使用Lift的卸载钩进行清理(请参见“提升关闭时运行代码”),然后在Heroku发送信号终止时安排挂钩:
Runtime
.
getRuntime
().
addShutdownHook
(
new
Thread
{
override
def
run
()
{
LiftRules
.
unloadHooks
.
toList
.
foreach
{
f
=>
tryo
{
f
()
}
}
}
})
这种处理SIGTERM
可能是一个惊喜,但是如果我们看看应用程序在Heroku上的运行方式,事情变得更加清晰。dyno是资源分配(512 MB内存),允许任意命令运行。正在运行的命令是启动“webapp runner”包的Java进程。你可以通过两种方式看到这一点。首先,如果你将shell绑定到你的dyno,你会看到一个WAR文件以及一个JAR文件:
$ heroku run bash --app glacial-waters-6292 Running `bash` attached to terminal... up, run.8802 ~ $ ls Procfile myapp-0.0.1.war webapp-runner-7.0.29.3.jar
其次,通过查看执行的进程:
$ heroku ps --app glacial-waters-6292 === web: `${PRE_JAVA}java ${JAVA_OPTS} -jar webapp-runner-7.0.29.3.jar --port ${PORT} ${WEBAPP_RUNNER_OPTS} myapp-0.0.1.war` web.1: up 2013/01/01 22:37:35 (~ 31s ago
这里我们看到一个Java进程执行一个名为webapp-runner- 7.0.29.3.jar的JAR文件,它将 WAR文件作为参数传递。这与您可能会更加熟悉的Tomcat catalina.sh脚本不同,而是这个启动程序。由于它没有注册处理程序来处理SIGTERM
,所以如果我们需要在关机期间释放任何资源。
所有这一切意味着如果您要以不同的方式启动Lift应用程序,您可以。您需要包装一个适当的容器(例如Jetty或Tomcat),并main
为Heroku 提供一种方法来调用。这有时被称为无容器部署。
如果您不是Heroku Enterprise Java客户,并且您对deploy:war
插件的不受支持的性质感到不舒服,那么现在您可以以支持的方式了解您需要执行的操作:提供main
启动应用程序并侦听连接的方法。“See Also”部分提供了如何做到这一点的指针。
数据库访问在Heroku
Heroku不限制您可以从Lift应用程序连接到哪些数据库,但是通过将免费数据库附加到您创建的应用程序,他们尝试使其PostgreSQL服务变得更加容易。
$ heroku pg --app glacial-waters-6292
=== HEROKU_POSTGRESQL_BLACK_URL (DATABASE_URL)
Plan: Dev
Status: available
Connections: 0
PG Version: 9.1.6
Created: 2012-12-31 10:02 UTC
Data Size: 5.9 MB
Tables: 0
Rows: 0/10000 (In compliance)
Fork/Follow: Unsupported
数据库的URL作为DATABASE_URL
环境变量提供给Lift 应用程序。它将具有如下这样的值:
postgres:// gghetjutddgr:RNC_lINakkk899HHYEFUppwG@ec2-54-243-230-119.compute-1。 amazonaws.com:5432/d44nsahps11hda
此URL包含用户名,密码,主机和数据库名称,但需要被操纵以供JDBC使用。为此,您可以在Boot.scala中包含以下内容:
Box
!!
System
.
getenv
(
"DATABASE_URL"
)
match
{
case
Full
(
url
)
=>
initHerokuDb
(
url
)
case
_
=>
// configure local database perhaps
}
def
initHerokuDb
(
dbInfo
:
String
)
{
Class
.
forName
(
"org.postgresql.Driver"
)
// Extract credentials from Heroku database URL:
val
dbUri
=
new
URI
(
dbInfo
)
val
Array
(
user
,
pass
)
=
dbUri
.
getUserInfo
.
split
(
":"
)
// Construct JDBC connection string from the URI:
def
connection
=
DriverManager
.
getConnection
(
"jdbc:postgresql://"
+
dbUri
.
getHost
+
':'
+
dbUri
.
getPort
+
dbUri
.
getPath
,
user
,
pass
)
SquerylRecord
.
initWithSquerylSession
(
Session
.
create
(
connection
,
new
PostgreSqlAdapter
))
}
在这里,我们正在测试DATABASE_URL
环境变量的存在,这表明我们在Heroku环境中。我们可以提取要使用的连接信息Session.create
。我们还需要完成“配置Squeryl和记录”addAround
中描述的通常配置。
为了运行,build.sbt需要适当的依赖关系Record和PostgreSQL:
...
"postgresql"
%
"postgresql"
%
"9.1-901.jdbc4"
,
"net.liftweb"
%%
"lift-record"
%
liftVersion
,
"net.liftweb"
%%
"lift-squeryl-record"
%
liftVersion
,
...
有了这一点,您的Lift应用程序可以使用Heroku数据库。您也可以从shell访问数据库,例如:
$ pg:psql --app glacial-waters-6292
psql (9.1.4, server 9.1.6)
SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
Type "help" for help.
d44nsahps11hda=> \d
No relations found.
d44nsahps11hda=> \q
$
要通过Heroku环境之外的JDBC工具进行访问,您需要包含强制SSL的参数。例如:
jdbc:postgresql://ec2-54-243-230-119.compute-1.amazonaws.com:5432 / d44nsahps11hda?username = gghetjutddgr&password = RNC_lINakkk899HHYEFUppwG&ssl = true&sslfactory = org.postgresql.ssl.NonValidatingFactory
也可以看看
Heroku 的Scala和Java文章,Dynos和Dyno Manager可以了解更多的这个食谱描述的细节。
JVM关闭钩子在JDK文档中有描述。
Heroku的无容器部署指南使用Maven打包应用程序。还有一个来自Matthew Henderson的模板SBT项目,包括一个JettyLauncher
类。
请求超时描述Heroku如何处理Comet长时间轮询。
跨多个服务器分发彗星
解
使用发布/订阅(pubsub)模型将每个服务器连接到主题,并将Comet消息传递到可以广播到所有应用程序一部分的服务器的主题。
您可以使用各种技术来完成此操作,例如数据库,消息系统和演员系统。对于这个配方,我们将使用RabbitMQ消息服务,但是在“另请参阅”部分中也有使用CouchDB和Amazon的简单通知服务的示例。
无论技术如何,原理如图10-2所示。将一个Lift应用程序发起的彗星事件发送到服务进行重新分配。这项服务的责任(图中标有“主题”),以确保所有参与的升降机应用程序都收到该事件。
第一步是下载并安装RabbitMQ。然后启动服务器:
$ ./sbin/rabbitmq-server -detatched
这个命令会在开始时产生各种消息,但最终会说:“代理运行”。
我们将用于演示pubsub模式的Lift应用程序是Simply Lift中描述的实时聊天应用程序。 第一个修改是将Lift模块与RabbitMQ进行通信。这是build.sbtlibraryDependencies
中的一行除外:
"net.liftmodules"
%%
"amqp_2.5"
%
"1.3"
AMQP代表高级消息队列协议,RabbitMQ会话协议。AMQP模块提供抽象的演员发送和接收消息,我们将实现这些演员,RemoteSend
并且RemoteReceiver
:
package
code.comet
import
net.liftmodules.amqp._
import
com.rabbitmq.client._
object
Rabbit
{
val
factory
=
new
ConnectionFactory
{
import
ConnectionFactory._
setHost
(
"127.0.0.1"
)
setPort
(
DEFAULT_AMQP_PORT
)
}
val
exchange
=
"lift.chat"
val
routing
=
""
val
durable
=
true
val
autoAck
=
false
object
RemoteSend
extends
AMQPSender
[
String
](
factory
,
exchange
,
routing
)
{
def
configure
(
channel
:
Channel
)
=
channel
.
exchangeDeclare
(
exchange
,
"fanout"
,
durable
)
}
object
RemoteReceiver
extends
AMQPDispatcher
[
String
](
factory
)
{
def
configure
(
channel
:
Channel
)
=
{
channel
.
exchangeDeclare
(
exchange
,
"fanout"
,
durable
)
val
queueName
=
channel
.
queueDeclare
().
getQueue
()
channel
.
queueBind
(
queueName
,
exchange
,
routing
)
channel
.
basicConsume
(
queueName
,
autoAck
,
new
SerializedConsumer
(
channel
,
this
)
)
}
}
}
此代码正在建立RemoteSend
,并通过RabbitMQ RemoteReceiver
对String
值进行序列化。接下来的“讨论”部分将探讨该代码。
为了利用这一点并通过RabbitMQ路由Comet消息,我们需要进行两个更改。在Boot.scala中,我们需要开始监听RabbitMQ的消息:
RemoteReceiver
!
AMQPAddListener
(
ChatServer
)
这是ChatServer
作为AMQP消息的侦听器附加的RemoteReceiver
。
最后的改变就是ChatServer
它本身。常规行为ChatServer
是String
从客户端接收消息并更新连接到Comet服务器的所有屏幕:
override
def
lowPriority
=
{
case
s
:
String
=>
msgs
:+=
s
;
updateListeners
()
}
通过RabbitMQ路由消息的更改是将任何String
从客户端重定向到RabbitMQ,并处理来自RabbitMQ的任何AMQP消息,并更新所有客户端:
override
def
lowPriority
=
{
case
AMQPMessage
(
s
:
String
)
=>
msgs
:+=
s
;
updateListeners
()
case
s
:
String
=>
RemoteSend
!
AMQPMessage
(
s
)
}
此更改意味着我们所有的Comet聊天消息都会发送到RabbitMQ,并将其分发到我们的Lift应用程序的所有实例,并且所有的实例都会收到消息作为AMQPMessage
实例,并正常更新聊天客户端。
讨论
要在本地运行Lift应用程序的多个实例,您需要正常启动SBT,然后在另一个终端中重新启动,但需要使用不同的端口号:
$ sbt ... > set port in container.Configuration := 9090 [info] Reapplying settings... [info] Set current project to RabbitMQ Chat (in build file:rabbitmq_chat/) > container:start
然后,您可以访问http://127.0.0.1:8080的一个应用程序,另一个在http://127.0.0.1:9090。
在示例代码中,您可以看到AMQPSender[T]
并AMQPDispatcher[T]
为我们处理大部分工作,并提供一些配置。在RemoteSend
我们正在配置AMQPSender
使用String
消息和使用调用的交换机的情况下lift.chat
。在RabbitMQ中,交换是我们发送消息的实体,交换机有责任传递消息。在这种情况下,交换是一个扇出(一种简单的主题),每个用户收到发送到交换机的任何消息的副本。这显然是我们希望将聊天消息发送到聊天应用程序的所有连接的Lift实例。
在RemoteReceiver
也被配置成接收String
消息,虽然配置稍长。在这里,以及指示要使用的交换,我们为我们的Lift实例声明一个临时队列。队列是RabbitMQ发送消息的地方,我们在这里说的是每个接收器都有自己的队列。扇出交换将确保发送到交换机的任何消息都被放入每个队列中。队列具有由RabbitMQ分配的随机名称,当我们断开连接时它被破坏。
最后一部分RemoteReceiver
是指定消费方式。默认的行为RemoteSend
是串行化对象,所以我们通过使用SerializedConsumer
AMQP模块提供的类在接收方中镜像它。
要查看RabbitMQ的行为,安装管理Web控制台很有用。从您安装RabbitMQ的目录:
$ ./sbin/rabbitmq-plugins enable rabbitmq_management
访问管理Web界面http://127.0.0.1:15672/并登录。默认用户名和密码为“guest”。
需要在开发过程中运行RabbitMQ(或其他类型的pubsub解决方案)可能不方便。在这种情况下,您可以简单地在Boot.scala中初始化服务:
if
(
Props
.
productionMode
)
RemoteReceiver
!
AMQPAddListener
(
ChatServer
)
而在聊天服务器中,只发送给本地客户端:
override
def
lowPriority
=
{
case
AMQPMessage
(
s
:
String
)
=>
msgs
:+=
s
;
updateListeners
()
case
s
:
String
=>
if
(
Props
.
productionMode
)
RemoteSend
!
AMQPMessage
(
s
)
else
{
msgs
:+=
s
;
updateListeners
()
}
}
请注意,Props.productionMode
是true
对的运行模式Production
,Staging
和Pilot
。
也可以看看
Lift聊天示例在Simply Lift中描述。此配方中使用的源代码位于GitHub上。
LiftAMQP模块的源可以在GitHub上找到。
如果您想了解更多关于RabbitMQ的信息,请查看教程或Alvaro Videla和Jason JW Williams的“ RabbitMQ在行动:每个人的分布式消息传递”(Manning Publications Co.)。
Diego Medina已经使用CouchDB实现了一个分布式的Comet解决方案,并在博客文章中进行了描述。
亚马逊的简单通知服务(SNS)是一个迷人的设施,因此也可以用于实现这种模式。您可以在GitHub上找到SNS的Lift模块。