回大东北的路上,思考了这么两件事:
思考这两个问题让自己一宿没睡,凌晨2点多起来开始代码实现。因为以前其实已经有非常多的积累了,比如我很早很早就有开发web框架的经验(serviceframework),所以这个系统也快速的被开发出来。
重新审视我们的Web系统
如果开发速度到极致,那么最好的场景就是我写完业务逻辑代码就已经可以对外提供服务了。不需要开发和业务逻辑无关的代码,不需要走jekins流程,不需要复杂的上线流程,不需要去DBA那申请数据库等等。对于一个Web服务而言,其核心就是action,也是交互的入口。其他所有的比如传统的service层,dao层,都是为了方便从action剥离代码,简化action提出的概念,因此,三者不在是平行关系,也不是上下层级关系,service,data就是action的utils类而已。所以我们的web框架应该极致简化,就是开发一个action,一个action就是一个普通的方法。action的接收和返回的参数都应该足够简单。
class RegisterPythonAction extends CustomAction {
override def run(params: Map[String, String]): String = {}
}
第二个是数据库层面,以前我们会讨论所谓贫血模型,充血模型,从美感上充血模型是漂亮的,但是从简单程度程度来看,贫血模型是最好的。数据库表在我们看来,就是一堆的case class,我们通过一些库来操作这些case class,我们应该以更加SQL的思维去做,而不是传统ORM的思维。在ORM中,所谓级联关系,所谓关联关系都是徒增复杂度而已。所以当我们应用这些case class的时候,我们完全套用SQL 语法去做。譬如:
ctx.run(
query[ScriptUserRw].
filter(_.mlsqlUserId == lift(user.id)).
join(query[ScriptFile]).on {
case (surw, sf) => surw.scriptFileId == sf.id
}.map {
case (_, sf) =>
sf
}
).toList
这看起来更像SQL语言的程序版本。当我们获得一个ScriptUserRw对象时,当我们要更新这个对象,我们依然需要用传统的SQL语言方式,而非给ScriptUserRw搞个save方法之类的,因为这意味着非常复杂的对象状态维护。而且我们也可以动态添加一些case class,接着就可以直接用来操作数据库,而不用受限于过于强制的绑定策略,对象状态维护。
技术都是螺旋式的发展,我们一开始拥抱数据库(SQL),一切都放在数据库上,接着拥抱ORM,一切都放在ORM框架层,现在,我们应该拥抱一个更平衡的状态。我认为quill是一个非常好的状态,也就是我们前面说的方案。
第三个是运行时层面。我们知道,Java/Scala/Python都运行在JVM上。但是JVM是个太通用的东西,所以我们需要一个应用层的虚拟机,或者说Runtime, 这个Runtime可以运行符合其规范的Plugin,运行的方式可以分成两种,第一种是启动Plugin的时候同时启动Runtime,第二种是启动Runtime之后,动态按需加载Plugin. 前面我们提到,一个Plugin既可以是一个具体的业务代码实现,也可以是一个平台框架。
这里再次体现技术的螺旋式发展,我们很早的时候是将代码放到web容器里运行,比如tomcat, weblogic, 后来我们将web容器放进了应用里。现在我们再次提供了一个包含了web容器的应用运行时。
有了这三方面加持,比如我要开发一个hello world程序,我只要实现一个action方法,就可以运行在已有的runtime或者运行这个action的同时启动一个新的runtime. 第一种模式可以让我们避开所谓的上线流程。当然了,我们可以在runtime之上开发了插件,这些插件允许用户动态注入源码(scala/python/java),然后这些源码就可以对外提供服务了。本质上我们通过runtime让一个进程成为一个Fat进程,尽管如此,插件也应该允许以新进程的方式运行,只要我们解决他们的通讯问题即可。
大家可能会疑惑这种方式的稳定性,其实这对于原型系统而言,是完全可以接受的,我们只是为了让大家尽快看到产品到底是什么样子的,或者给一小部分用户使用,亦或是为了快速的给其他的产品或者业务开发一个定制的接口(开发时间应该小时甚至分钟计)。
具体实现架构
为了达到上述目标,我设计了下面三样东西:
对于数据库应用,我们开发了一个基础插件app_runtime_with_db,该插件既可以作为插件运行,同时也可以可以作为其他依赖于数据库的web应用的依赖库,进一步方便基于数据库应用插件的开发。app_runtime_withdb 需要用户在配置文件里配置一个基础数据库,该数据库里只有一张表,该表其实就是一个kv,记录不同插件对数据库的需求,比如特定插件需要的数据库地址。这意味着,一个app_runtime上可以动态链接N个数据库,也非常方便后续那种动态编程的插件去连接新的数据库。
我们还开发了一个ar_python插件,该插件允许用户编写python代码,并且注册到系统里,从而实现通过python实现action。值得注意的是,该插件依赖于app_runtime_with_db。
同样的,因为我们在app_runtime系统里,每个插件可以连接相同的数据库,也可以连接各自的数据库,所以我们可以做很好的系统切分,比如我们还设计了user_system插件,实现注册,权限等相关功能,其他系统可以基于该插件继续开发自己的功能,并且拥有自己的数据库。
下面我们介绍如何开发ar_python插件,来介绍web-platform的开发流程。
示例
首先pip安装如下命令集:
pip install watchdog sfcli
sfcli是我们用Python开发的一个辅助开发web-platform的命令行系统。
我们开发的项目名字叫:ar_python,该项目如前所述主要是接受用户传入python代码,之后用户可以指定用哪个python代码处理请求。我们先创建项目:
sfcli create --name ar_python
这是一个maven项目,通过如下命令用idea打开:
idea ar_python
该项目默认就包含两个子模块,ar_python-bin模块主要是插件描述的代码,ar_python-lib则是主要业务逻辑的项目。我们大部分情况都是在lib项目里开发写代码。
因为ar_python需要存储python脚本,和传统采用配置文件连接数据不同的方案不同,基于app_runtime_with_db 插件开发的应用是采用数据库来做数据库配置的。app_runtime_with_db 有一个基础数据库 ,其实就是配置系统。其他的插件都是动态从该配置系统获取数据库配置。系统在创建时,会自动创建数据库连接类,大概长这个样子:
package tech.mlsql.app_runtime.example
import net.csdn.jpa.QuillDB
import tech.mlsql.app_runtime.db.quill_model.DictType
import tech.mlsql.app_runtime.db.service.BasicDBService
object PluginDB {
val plugin_name = "ar_python"
lazy val ctx = {
val dbName = BasicDBService.fetch(plugin_name, DictType.INSTANCE_TO_DB)
val dbInfo = dbName.getOrElse {
throw new RuntimeException(s"DB: cannot init db for plugin ${plugin_name} ")
}
val dbConfig = BasicDBService.fetchDB(dbInfo.value).getOrElse {
throw new RuntimeException(s"DB: cannot get db config for plugin ${plugin_name} ")
}
QuillDB.createNewCtxByNameFromStr(dbConfig.name, dbConfig.value)
}
}
使用时,先建立一个case class,
接着在MySQL中创建数据库mlsql_python_predictor 并且创建一个包含id,name,code 三个字段的表python_script.
现在我们已经可以操作数据库了。下面是我们开始写核心逻辑,注册新python代码的到数据里:
package tech.mlsql.app_runtime.python.action
import tech.mlsql.app_runtime.python.PluginDB.ctx
import tech.mlsql.app_runtime.python.PluginDB.ctx._
...
class RegisterPythonAction extends CustomAction {
override def run(params: Map[String, String]): String = {
val name = params("name")
val code = params("code")
def fetch = {
ctx.run(
ctx.query[quill_model.PythonScript].filter(_.name == lift(name))
).headOption
}
fetch match {
case Some(_) => ctx.run(ctx.query[quill_model.PythonScript].filter(_.name == lift(name)).update(_.code -> lift(code)))
case None => ctx.run(ctx.query[quill_model.PythonScript].insert(_.name -> lift(name), _.code -> lift(code)))
}
JSONTool.toJsonStr(List(fetch.get))
}
}
用户只要引入自动生成的PluginDB.ctx即可。
修改ar_python-bin里的PluginDesc描述文件,注册下我们写的Action:
所有代码工作完成。我们可以启动该插件进行测试,具体做法如下:
首先在项目主目录开启增量编译
sfcli compile --dev true
这个指令会监控项目任何变更,并且重新编译。
接着启动服务:
sfcli run --dev true
这个指令会让系统监控任何变更,并且热加载响应的代码变更(其实是自动重启,但是速度很快,大概几秒钟)
我们可以通过如下代码测试我们的接口:
import requests
datas = {"code": """from pyjava.api.mlsql import PythonContext
for row in context.fetch_once():
print(row)
# params = dict([(row["key"], row["value"]) for row in context.fetch_once_as_rows()])
context.build_result([{"content": "{}"}], 1)
""", "action": "registerPyAction", "name": "echo"}
r = requests.post("http://127.0.0.1:9007/run", data=datas)
print(r.text)
print(r.status_code)
如果发现有异常,可以直接修改代码,修改无需重启即可重新进行测试。如果需要debug,启动的容器支持标准的java debug,你可以用你的idea intellij 直接连接进行debug.
为了部署到一个正在运行的app_runtime容器里,我们需要得到这个插件的发型包,执行以下命令
sfcli release
该指令会产生一个release目录,里面会有一个包含依赖的jar包。
假设我们想在自己的电脑上从头开始启动一个app_runtime容器,可按如下步骤:
创建空的容器项目:
sfcli create --name container1 --empty true
进入该项目目录,到config里的application中去配置app_runtime_with_db需要的数据库,然后启动容器:
sfcli runtime
启动后,开始安装db插件:
sfcli plugin --add /Users/allwefantasy/CSDNWorkSpace/app_runtime_with_db/release/app_runtime_with_db-bin_2.11-1.0.0.jar
接着安装ar_python插件:
sfcli plugin --add /Users/allwefantasy/CSDNWorkSpace/ar_python/release/ar_python-bin_2.11-1.0.0.jar
当然,对于插件,我们也可以通过接口来完成:
http://127.0.0.1:9007/run?
action=registerPlugin
&url=/.../target/mlsql-python-predictor_2.11-1.0-SNAPSHOT.jar
&className=tech.mlsql.app_runtime.plugin.PluginDesc
因为ar_python-bin需要数据库,所以我们还需要为ar_python-bin注册下数据库:
import requests
request_url = "http://127.0.0.1:9007/run"
test_db_config = """
mlsql_python_predictor:
host: 127.0.0.1
port: 3306
database: mlsql_python_predictor
username: xxxxx
password: xxxxx
initialSize: 8
disable: true
removeAbandoned: true
testWhileIdle: true
removeAbandonedTimeout: 30
maxWait: 100
filters: stat,log4j
"""
def addDB(instanceName):
# user-system
datas = {"dbName": "mlsql_python_predictor", "instanceName": instanceName, "dbConfig": test_db_config}
r = http(addDB.__name__, datas)
printRespose(r)
addDB("ar_python")
现在你就可以访问python相关的功能了。
总结
从前面示例我们可以看到,我们可以开发一些平台插件,并且注册到已经运行的app-runtime实例里。本质上,app-runtime就是一些运行插件的容器。基于这些平台插件,我们可以更加便捷的开发各种简单的“脚本”即可完成的功能,而不用每次煞有介事的开发一个项目。如果你需要,你也可以正儿八经的开发一个应用插件完成你的具体业务功能。当你有一个app-runtime池子,这意味着我们可以随时将这些插件运行在一到N个实例里。 sfcli也极大的方便了开发,而每个plugin实际也可独立运行,便于在IDE中进行调试。未来给python,scala相关的plugin开发web界面,我们可以直接在plugin提供的web界面上写代码,从而上线新功能,免去了众多的开发流程,对于原型,简单的应用,以及一些非常紧急的场景都能带来极大的帮助。