diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 3fb8f3f..0050b8c 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,19 @@ -# Kungfu Panda -**Kungfu Panda** is a library for register python pandas UDFs in Spark SQL. +# Bamboo -# Quick Start +Spark运行MLFlow模型的缓存组件 -1. download project. -``` -git clone https://github.com/cfmcgrady/kungfu-panda.git -``` - -2. install python environment by conda. -``` -conda env create -f path/to/conda.yaml -p /tmp/kungfu-panda -``` +# 环境变量依赖 -3. train a Kmean classify model with mlflow. -``` -/tmp/kungfu-panda/bin/python path/to/train.py -``` +构建docker镜像和部署服务都需要依赖相关的环境变量。 -4. register model. -```scala - val spark = SparkSession - .builder() - .appName("kungfu panda example") - .master("local[4]") - .getOrCreate() +| 环境变量 | 说明 | +| --- | --- | +| AWS_ACCESS_KEY_ID | minio访问id,需要有mlflow的bucket权限| +| AWS_SECRET_ACCESS_KEY | minio访问key,需要有mlflow的bucket权限| +| MLFLOW_S3_ENDPOINT_URL | minio地址,外部测试时候使用http://minio.k8s.uc.host.dxy,部署到k8s的时候需要内部域名 | +| BAMBOO_CACHE_DIR | 缓存根路径,默认为/tmp/cache | - val python = "/tmp/kungfu-panda/bin/python" - val artifactRoot = "." - // find run id with mlflow. - val runid = "9c6c59d0f57f40dfbbded01816896687" - val pythonExec = Option(python) - PandasFunctionManager.registerMLFlowPythonUDF( - spark, "test", - returnType = Option(IntegerType), - artifactRoot = Option(artifactRoot), - runId = runid, - driverPythonExec = pythonExec, - driverPythonVer = None, - pythonExec = pythonExec, - pythonVer = None) - spark.sql( - """ - |select test(x, y) from ( - |select 1 as x, 1 as y - |) - |""".stripMargin) - .show() +build docker image: +```shell +docker build -t bamboo -f dev/bamboo/Dockerfile . ``` - -# Register Function With Spark SQL - -1. add parser extensions when we create `SparkSession` -```scala -val spark = SparkSession - .builder() - .appName("panda sql example") - .master("local[4]") - .withExtensions(CreateFunctionParser.extBuilder) - .getOrCreate() -``` - -2. register mlflow function. -```sql -CREATE FUNCTION `test` AS '${runid}' USING `type` 'mlflow', `returns` 'integer', `artifactRoot` '${artifactRoot}', `pythonExec` '${python}' -``` - -visit [PandaSqlExample](./examples/local/src/main/scala/org/panda/example/local/PandaSqlExample.scala) for full example. - -# Run On Yarn Cluster - -// todo diff --git a/assembly/pom.xml b/assembly/pom.xml index 426b2d0..d7bfc78 100644 --- a/assembly/pom.xml +++ b/assembly/pom.xml @@ -13,7 +13,9 @@ pom - ${project.parent.basedir}/libs + + tgz + * @@ -25,11 +27,10 @@ 3.0.0 - ${project.basedir}/src/main/assembly/assembly.xml - + ${project.basedir}/src/main/assembly/assembly.xml - ${project.basedir} + ${project.parent.basedir} diff --git a/assembly/src/main/assembly/assembly.xml b/assembly/src/main/assembly/assembly.xml index 3774a1d..7bd06e1 100644 --- a/assembly/src/main/assembly/assembly.xml +++ b/assembly/src/main/assembly/assembly.xml @@ -1,16 +1,33 @@ bin-${project.version} - tgz + ${assembly.format} - - - ${project.parent.basedir}/conf - conf + + + + true + - **/* + + org.panda:${assembly.target.module} - + + libs + ${assembly.unpack} + + + + + + + + + + + + + ${project.parent.basedir} . @@ -20,8 +37,8 @@ - ${project.parent.basedir}/libs - libs + ${project.parent.basedir}/conf + conf **/* diff --git a/bamboo/pom.xml b/bamboo/pom.xml index abbb032..edbbff0 100644 --- a/bamboo/pom.xml +++ b/bamboo/pom.xml @@ -14,6 +14,7 @@ ${project.parent.basedir}/libs + 2.0.2.RELEASE @@ -30,12 +31,17 @@ org.springframework.boot spring-boot-starter-jetty - 2.0.2.RELEASE + ${spring-boot.version} org.springframework.boot spring-boot-starter-web - 2.0.2.RELEASE + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot.version} io.springfox @@ -50,13 +56,23 @@ com.fasterxml.jackson.module jackson-module-scala_${scala.binary.version} - 2.9.9 com.fasterxml.jackson.core jackson-databind - 2.9.9.3 - + + + single-jar + + + + org.apache.maven.plugins + maven-shade-plugin + + + + + diff --git a/bamboo/src/main/scala/org/panda/bamboo/service/controller/CondaController.scala b/bamboo/src/main/scala/org/panda/bamboo/service/controller/CondaController.scala index c291578..ae7c02c 100644 --- a/bamboo/src/main/scala/org/panda/bamboo/service/controller/CondaController.scala +++ b/bamboo/src/main/scala/org/panda/bamboo/service/controller/CondaController.scala @@ -1,15 +1,20 @@ package org.panda.bamboo.service.controller -import java.util.Base64 +import java.nio.file.{Files, Paths} +import java.util.{Base64, HashMap => JMap} import scala.util.control.NonFatal import org.apache.catalina.servlet4preview.http.HttpServletRequest import org.apache.spark.panda.utils.Conda -import org.panda.bamboo.util.{CacheKey, CacheManager} +import org.panda.bamboo.util.{CacheKey, CacheManager, PythonEnvironmentResolvedPath} +import org.panda.Config +import org.slf4j.LoggerFactory import org.springframework.core.io.{Resource, UrlResource} import org.springframework.http.{HttpHeaders, MediaType, ResponseEntity} -import org.springframework.web.bind.annotation.{PathVariable, RequestBody, RequestMapping, RequestMethod, RequestParam, RestController} +import org.springframework.web.bind.annotation.{PathVariable, PostMapping, RequestBody, RequestMapping, RequestMethod, RequestParam, RestController} +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.servlet.mvc.support.RedirectAttributes /** * @time 2019-08-30 10:23 @@ -19,6 +24,8 @@ import org.springframework.web.bind.annotation.{PathVariable, RequestBody, Reque @RequestMapping(value = Array("/api/v1/conda")) class CondaController { + private val logger = LoggerFactory.getLogger(getClass.getCanonicalName) + /** * post method for createAndGet. * @param yaml @@ -32,7 +39,7 @@ class CondaController { println(yaml) // scalastyle:on val name = CacheManager.get(key(yaml)) - val resource = new UrlResource(CacheManager.getFileByName(name).toUri) + val resource = new UrlResource(PythonEnvironmentResolvedPath.compressFilePath(name).toUri) var contentType = "" @@ -102,7 +109,7 @@ class CondaController { def directGet(@PathVariable filename: String, request: HttpServletRequest): ResponseEntity[Resource] = { - val file = s"file:///tmp/cache/${filename}/${filename}.tgz" + val file = s"file://${Config.CACHE_ROOT_DIR}/${filename}/${filename}.tgz" val resource = new UrlResource(file) var contentType = "" try { @@ -133,6 +140,38 @@ class CondaController { Response(data = Map("result" -> Base64.getEncoder.encodeToString(yaml.getBytes("utf-8")))) } + @RequestMapping(value = Array("/admin/remove/{md5}"), method = Array(RequestMethod.DELETE)) + def remove(@PathVariable md5: String): Response = { + try { + CacheManager.remove(CacheKey(md5, new JMap[String, Object]())) + Response() + } catch { + case t: Throwable => + logger.error(s"catch an exception when remove environment $md5", t) + Response(stat = false, message = t.getMessage) + } + } + + @PostMapping(value = Array("admin/upload")) + def manullyUpload(@RequestParam("file") file: MultipartFile, + redirectAttributes: RedirectAttributes): Unit = { + if (file.isEmpty()) { + redirectAttributes.addFlashAttribute("message", "Please select a file to upload") + return "redirect:uploadStatus" + } + try { + // Get the file and save it somewhere + val bytes = file.getBytes() + val path = Paths.get("/tmp/dd" + file.getOriginalFilename()) + Files.write(path, bytes) + redirectAttributes.addFlashAttribute("message", + "You successfully uploaded '" + file.getOriginalFilename() + "'") + } catch { + case e => e.printStackTrace() + } + return "redirect:/uploadStatus"; + } + private def key(yaml: String): CacheKey = { val ymap = Conda.normalize(yaml) CacheKey(ymap.getOrDefault("name", "").asInstanceOf[String], ymap) diff --git a/bamboo/src/main/scala/org/panda/bamboo/service/controller/MLFlowArtifactController.scala b/bamboo/src/main/scala/org/panda/bamboo/service/controller/MLFlowArtifactController.scala index 952736f..ef0d18e 100644 --- a/bamboo/src/main/scala/org/panda/bamboo/service/controller/MLFlowArtifactController.scala +++ b/bamboo/src/main/scala/org/panda/bamboo/service/controller/MLFlowArtifactController.scala @@ -2,8 +2,8 @@ package org.panda.bamboo.service.controller import java.net.URI import java.nio.file.Paths - import javax.servlet.http.HttpServletRequest + import org.panda.bamboo.util.{CacheManager, MLFlowRunCacheKey} import org.springframework.core.io.{Resource, UrlResource} import org.springframework.http.{HttpHeaders, MediaType, ResponseEntity} @@ -48,7 +48,7 @@ class MLFlowArtifactController { .body(resource) } - @RequestMapping(value = Array("/admin/remove/{runid}"), method = Array(RequestMethod.GET, RequestMethod.POST)) + @RequestMapping(value = Array("/admin/remove/{runid}"), method = Array(RequestMethod.DELETE)) def remove(@PathVariable runid: String): Response = { try { CacheManager.remove(key(runid)) diff --git a/bamboo/src/main/scala/org/panda/bamboo/util/CacheEntity.scala b/bamboo/src/main/scala/org/panda/bamboo/util/CacheEntity.scala index 0df60ee..3d2a835 100644 --- a/bamboo/src/main/scala/org/panda/bamboo/util/CacheEntity.scala +++ b/bamboo/src/main/scala/org/panda/bamboo/util/CacheEntity.scala @@ -8,8 +8,10 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantReadWriteLock import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.io.FileUtils import org.apache.commons.logging.LogFactory -import org.apache.spark.panda.utils.{CompressUtil, Conda, SFTPUtil, Util} +import org.apache.spark.panda.utils.{CompressUtil, Conda, MLFlowMinioUtilImpl, MLFlowUtil, SFTPUtil, Util} +import org.panda.Config /** * @time 2019-09-12 16:48 @@ -18,7 +20,7 @@ import org.apache.spark.panda.utils.{CompressUtil, Conda, SFTPUtil, Util} trait CacheEntity[T] { private val _lock = new ReentrantReadWriteLock() - private val _cacheVaild: AtomicBoolean = new AtomicBoolean(false) + private val _cacheValid: AtomicBoolean = new AtomicBoolean(false) protected def write: Unit @@ -30,7 +32,9 @@ trait CacheEntity[T] { _lock.writeLock().lock() try { delete - _cacheVaild.set(false) + _cacheValid.set(false) + } catch { + case t: Throwable => throw t } finally { _lock.writeLock().unlock() } @@ -39,15 +43,15 @@ trait CacheEntity[T] { def get(): T = { _lock.readLock().lock() - if (!_cacheVaild.get()) { + if (!_cacheValid.get()) { _lock.readLock().unlock() _lock.writeLock().lock() try { - if (!_cacheVaild.get()) { + if (!_cacheValid.get()) { // do package download - // TODO:(fchen) throws execption when we has downloaded fail. + // TODO:(fchen) throws exception when we has downloaded fail. write - _cacheVaild.set(true) + _cacheValid.set(true) } _lock.readLock().lock() } finally { @@ -67,15 +71,18 @@ class PythonEnvironmentCacheEntity ( name: String, configuration: JMap[String, Object]) extends CacheEntity[String] { + import CacheManager._ + val logger = LogFactory.getLog(this.getClass) + val resolvedEnvRootPath: Path = PythonEnvironmentResolvedPath.resolveEnvPath(name) + private def downloadAndPackage(): Unit = { - import CacheManager._ - if (!(Paths.get(basePath, Array(name): _*).toFile.exists() && - Paths.get(basePath, Array(name, s"${name}.tgz"): _*).toFile.exists())) { + if (!resolvedEnvRootPath.toFile.exists() && + !PythonEnvironmentResolvedPath.compressFilePath(name).toFile.exists()) { logger.info("env not found, begin download from internet.") // the environment has never been download before, so we download this package now. - val envpath = Conda.createEnv(name, configuration, basePath + File.separator + name) + val envpath = Conda.createEnv(name, configuration, resolvedEnvRootPath.toString) // make python command executable. val makeExecutable = { @@ -95,7 +102,10 @@ class PythonEnvironmentCacheEntity ( override protected def read: String = name - override def delete: Unit = throw new UnsupportedOperationException("") + override def delete: Unit = { + FileUtils.deleteDirectory(PythonEnvironmentResolvedPath.resolveEnvPath(name).toFile) +// Util.recursiveDeleteFile(envRootPath) + } /** * . @@ -106,53 +116,66 @@ class PythonEnvironmentCacheEntity ( */ } -class MLFlowRunCacheEntity(runid: String) extends CacheEntity[String] { +class MLFlowRunCacheEntity(runid: String) extends CacheEntity[String] + with MLFlowUtil + with ResolvedPath { + private val logger = LogFactory.getLog(getClass) -// private lazy val ARTIFACT_ROOT = new URI(sys.env.getOrElse("MLFLOW_ARTIFACT_ROOT", throw new RuntimeException(""))) - private lazy val ARTIFACT_ROOT = resolveURI("/Users/fchen/Project/python/mlflow-study/mlruns") + private lazy val MLFLOW_TRACKING_URI = sys.env.getOrElse("MLFLOW_TRACKING_URI", + throw new IllegalArgumentException("please set MLFLOW_TRACKING_URI environment variable.") + ) + + override def mlflowTrackingUri: String = MLFLOW_TRACKING_URI + + logger.info(s"service start with MLFLOW_TRACKING_URI = ${mlflowTrackingUri}") - private lazy val BASE_PATH = "/tmp/runs" + private val resolvedRunPath = resolveRunPath(runid) + + private val resolvedCompressionPath = compressFilePath(runid) override protected def write: Unit = { // make sure this run was not downloaded before. - if (new File(compressFilePath).getParentFile.exists()) { + if (new File(compressFilePath(runid)).getParentFile.exists()) { return } - logger.info(s"start to download run ${runid} from artifact ${ARTIFACT_ROOT}") - val path = ARTIFACT_ROOT.getScheme.toLowerCase match { + artifactUri(runid) match { + case Right(uri) => + logger.info(s"start to download run ${runid} from artifact ${uri}") + downloadAndCompress(new URI(uri)) + case Left(e) => + throw e + } + } + + def downloadAndCompress(uri: URI): Unit = { + uri.getScheme.toLowerCase match { case "file" => - Util.getArtifactByRunId(ARTIFACT_ROOT.getPath, runid) + val path = Util.getArtifactByRunId(uri.getPath, runid) + CompressUtil.tar2(path, resolvedCompressionPath) case "sftp" => // TODO:(fchen) we should look the remote path from mlflow tracking server api. - val remotePath = ARTIFACT_ROOT.getPath + "/0/" + runid - SFTPUtil.download(ARTIFACT_ROOT.getHost, remotePath, "/tmp/runs") - s"${BASE_PATH}/$runid" + val remotePath = uri.getPath + "/0/" + runid + // make sure the `BASE_PATH` exist. otherwise we should create the directory manually. + Util.mkdir(resolvedRunPath) + + SFTPUtil.download(uri.getHost, remotePath, resolvedRunPath) + CompressUtil.tar2(resolvedRunPath, resolvedCompressionPath) + case "s3" => + // make sure the `BASE_PATH` exist. otherwise we should create the directory manually. + Util.mkdir(resolvedRunPath) + MLFlowMinioUtilImpl.downloadAsZip(uri.getHost, uri.getPath, resolvedCompressionPath) case _ => throw new UnsupportedOperationException() } - CompressUtil.tar(path, compressFilePath) } - def compressFilePath: String = { - s"${BASE_PATH}/${runid}/${runid}.tgz" - } - - override protected def read: String = compressFilePath + override protected def read: String = compressFilePath(runid) override def delete: Unit = { - try { - Util.recursiveListFiles(Paths.get(s"${BASE_PATH}/${runid}").toFile) - .foreach(f => { - Files.deleteIfExists(f.toPath) - }) - Files.deleteIfExists(Paths.get(s"${BASE_PATH}/${runid}")) - } catch { - case e: IOException => - logger.info(s"remove run $runid failed!", e) - } + Util.recursiveDeleteFile(resolvedRunPath) } private def resolveURI(path: String): URI = { @@ -173,5 +196,34 @@ class MLFlowRunCacheEntity(runid: String) extends CacheEntity[String] { } new File(path).getAbsoluteFile().toURI() } +} + +trait ResolvedPath { + self: MLFlowRunCacheEntity => + private lazy val BASE_PATH = sys.env.getOrElse("panda.cache.dir", s"${Config.CACHE_ROOT_DIR}/panda/runs") + + /** + * the root cache path of this run. + */ + protected val resolveRunPath = (runid: String) => s"${BASE_PATH}/${runid}" + + /** + * the compressed file path of this run. + */ + val compressFilePath = { + runid: String => s"${resolveRunPath(runid)}/${runid}.tgz" + } + +} + +object PythonEnvironmentResolvedPath { + // // TODO:(fchen) generate base path with server info(hostname: port). + // // so that we can deploy multi server on the same host. + // val basePath = Config.CACHE_ROOT_DIR + private lazy val BASE_PATH = s"${Config.CACHE_ROOT_DIR}/conda" + + val resolveEnvPath = (name: String) => Paths.get(BASE_PATH, Array(name): _*) + + val compressFilePath = (name: String) => Paths.get(BASE_PATH, Array(name, s"${name}.tgz"): _*) } diff --git a/bamboo/src/main/scala/org/panda/bamboo/util/CacheManager.scala b/bamboo/src/main/scala/org/panda/bamboo/util/CacheManager.scala index e3b7f76..9fa6371 100644 --- a/bamboo/src/main/scala/org/panda/bamboo/util/CacheManager.scala +++ b/bamboo/src/main/scala/org/panda/bamboo/util/CacheManager.scala @@ -2,10 +2,12 @@ package org.panda.bamboo.util import java.nio.file.{Path, Paths} import java.util.{Map => JMap} +import java.util.concurrent.locks.ReentrantLock import com.google.common.cache.{CacheBuilder, CacheLoader} import org.apache.juli.logging.LogFactory import org.apache.spark.panda.utils.Conda +import org.panda.Config /** * @time 2019-08-29 14:33 @@ -15,28 +17,10 @@ object CacheManager { private val logger = LogFactory.getLog(getClass) - private lazy val _pythonEnvCache = CacheBuilder.newBuilder() - .maximumSize(100) - .build( - new CacheLoader[CacheKey, PythonEnvironmentCacheEntity] { - override def load(k: CacheKey): PythonEnvironmentCacheEntity = { - new PythonEnvironmentCacheEntity(k.name, k.conf) - } - } - ) - - private lazy val _mlflowRunCache = CacheBuilder.newBuilder() - .maximumSize(100) - .build( - new CacheLoader[MLFlowRunCacheKey, MLFlowRunCacheEntity] { - override def load(k: MLFlowRunCacheKey): MLFlowRunCacheEntity = { - new MLFlowRunCacheEntity(k.runid) - } - } - ) - + private val _lock = new ReentrantLock() + // todo:(fchen) 基于文件锁来实现 private lazy val _cache = CacheBuilder.newBuilder() - .maximumSize(1000) + .maximumSize(100000) .build( new CacheLoader[Key, CacheEntity[String]] { override def load(key: Key): CacheEntity[String] = { @@ -51,7 +35,9 @@ object CacheManager { ) def get: (Key) => String = { - key => _cache.get(key).get() + withLock { + key => _cache.get(key).get() + } // case k: CacheKey => // _pythonEnvCache.get(k).get() // case k: MLFlowRunCacheKey => @@ -59,46 +45,21 @@ object CacheManager { } def remove: (Key) => Unit = { - key => - logger.info(s"start to remove ${key}") - _cache.get(key).remove - } - - /** - * get the tar archive file path by environment name. - */ - def getFileByName(name: String): Path = { - Paths.get(basePath, Array(name, s"${name}.tgz"): _*) + withLock { + key => + logger.info(s"start to remove ${key}") + _cache.get(key).remove + _cache.invalidate(key) + } } - // TODO:(fchen) generate base path with server info(hostname: port). - // so that we can deploy multi server on the same host. - val basePath = "/tmp/cache" - - def main(args: Array[String]): Unit = { - val yaml = - """ - |channels: - | - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/main/ - | - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/free/ - | - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/r/ - | - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/pro/ - | - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/msys2/ - |dependencies: - |- python=3.6.0 - |- numpy - |name: conda-test - """.stripMargin - - (1 to 10).foreach(i => { - new Thread(new Runnable { - override def run(): Unit = { - val ymap = Conda.normalize(yaml) - val k = CacheKey(ymap.getOrDefault("name", "").asInstanceOf[String], ymap) - get(k) - } - }).start() - }) + private def withLock[T](f: T): T = { + _lock.lock() + try { + f + } finally { + _lock.unlock() + } } } diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..4b39352 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,156 @@ +#!/bin/bash + +APP_NAME=Merlion +APP_VERSION=1.0 +JAR_NAME=${APP_NAME}-${APP_VERSION}.jar +#export JAVA_HOME=/opt/java8/ + +# 启动时间版本 + +DATE_VERSION=$(date +%Y%m%d%H%M%S) + +# 生产配置 +BIN_DIR=/usr/local/release/${APP_NAME}/ +PID_FILE=/tmp/${APP_NAME}.pid +LOG_FILE=/tmp/${APP_NAME}.${DATE_VERSION}.log +MAIN_CLASS=com.dxy.data.merlion.service.DevApplication + +export GLOG_v=1 +export GLOG_log_dir=/tmp/a +# export LIBPROCESS_IP=10.25.26.135 + +# 自定义配置 +export MESOS_NATIVE_JAVA_LIBRARY=/usr/local/lib/libmesos.so + +if [ -z "${DXY_PROJECT_HOME}" ]; then + export DXY_PROJECT_HOME="$(cd "`dirname "$0"`"/.; pwd)" +fi + +JAVA_OPTS="-Dserver.port=8100 -Dfile.encoding=UTF-8 -Dlog4j.configuration=file://${DXY_PROJECT_HOME}/conf/log4j.properties -Dlog4j.configurationFile=file://${DXY_PROJECT_HOME}/conf/log4j2.xml" + +JAVA_OPTS=${JAVA_OPTS}" -Dsentry.dsn=https://87727747fd244d61a8dda6339cb1b657:48ed7ab92a774313bca9e326653c4831@sentry.k8s.uc.host.dxy/82" + + +JAVA_OPTS=${JAVA_OPTS}" -Xms8g -Xmx8g \ + -XX:ParallelGCThreads=8 \ + -XX:SurvivorRatio=1 \ + -XX:LargePageSizeInBytes=128M \ + -XX:MaxNewSize=1g \ + -XX:CMSInitiatingOccupancyFraction=80 \ + -XX:+UseCMSCompactAtFullCollection \ + -XX:CMSFullGCsBeforeCompaction=0 \ + -XX:-UseGCOverheadLimit \ + -XX:MaxTenuringThreshold=5 \ + -XX:GCTimeRatio=19 \ + -XX:+UseConcMarkSweepGC \ + -XX:+UseParNewGC \ + -XX:+PrintGCDetails \ + -XX:+PrintGCTimeStamps \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:HeapDumpPath=/tmp/${APP_NAME}-${MODULE}.dump \ + -Xloggc:/tmp/${APP_NAME}-${MODULE}-gc.$DATE_VERSION.log" +JARS=$(echo ${DXY_PROJECT_HOME}/libs/*.jar | tr ' ' ':') + +function status() { + echo "$APP_NAME Status" + if [ -s ${PID_FILE} ]; then + ps h -fp $(cat ${PID_FILE}) + fi +} + +function common_run() { + $JAVA_HOME/bin/java -cp $JARS "$@" +} + +function package() { + cd $DXY_PROJECT_HOME + echo "Remove ${APP_NAME}-bin-${APP_VERSION}.tgz..." + rm ${APP_NAME}-bin-${APP_VERSION}.tgz + echo "Package App $APP_NAME-$APP_VERSION" + mvn clean package -DskipTests "$@" + mv $DXY_PROJECT_HOME/assembly/${APP_NAME}-bin-${APP_VERSION}.tgz $DXY_PROJECT_HOME +} + +function usage() { +cat << EOF + Usage: ./bootstrap.sh package +EOF +} + +function echo_build_properties() { + echo version=$APP_VERSION + echo user=$USER + echo revision=$(git rev-parse HEAD) + echo branch=$(git rev-parse --abbrev-ref HEAD) + echo date=$(date +"%Y/%m/%d %H:%M:%S") + echo url=$(git config --get remote.origin.url) +} + +function build_info() { + echo_build_properties $2 > $DXY_PROJECT_HOME/INFO +} + +function start_frontend() { + $JAVA_HOME/bin/java $JAVA_OPTS -cp $JARS $MAIN_CLASS +} + +function start() { + echo "Start App $APP_NAME" + if [ -s ${PID_FILE} ]; then + r=`ps h -fp $(cat ${PID_FILE})` + fi + if [ "$r" == "" ]; then + nohup $JAVA_HOME/bin/java $JAVA_OPTS -cp $JARS $MAIN_CLASS > ${LOG_FILE} 2>&1 & echo $! > ${PID_FILE} + echo "${APP_NAME} log file ${LOG_FILE}" + else + echo "${APP_NAME} already running..." + fi +} + +function stop() { + echo "Stop App $APP_NAME" + if [ -s ${PID_FILE} ]; then + echo "stopping ${APP_NAME}: $(cat ${PID_FILE})" + kill -9 $(cat ${PID_FILE}) + rm -f ${PID_FILE} + else + echo "pid file not found" + exit 1 + fi +} + +function restart() { + stop + start +} + +case "$1:$2:$3" in + package:*) + package "${@:2}" + ;; + run:*:*) + common_run "${@:2}" + ;; + build_info:*:*|bi:*:*) + build_info + ;; + start_frontend:*) + start_frontend + ;; + start:*) + start + ;; + stop:*) + stop + ;; + restart:*) + restart + ;; + h|help) + usage + ;; + *) + usage + exit 0 +esac + diff --git a/common/pom.xml b/common/pom.xml index a231c62..187167b 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ org.apache.commons commons-compress - 1.18 + 1.19 org.yaml @@ -37,6 +37,19 @@ sshj 0.27.0 + + commons-io + commons-io + 2.6 + + + org.mlflow + mlflow-client + + + io.minio + minio + diff --git a/common/src/main/java/org/apache/spark/panda/utils/GZIPUtil.java b/common/src/main/java/org/apache/spark/panda/utils/GZIPUtil.java index 8c455d3..0b42e69 100644 --- a/common/src/main/java/org/apache/spark/panda/utils/GZIPUtil.java +++ b/common/src/main/java/org/apache/spark/panda/utils/GZIPUtil.java @@ -37,6 +37,7 @@ public static void createTarArchive(String parentDir, String outFile){ e.printStackTrace(); }finally{ try { + tarArchive.finish(); tarArchive.close(); } catch (IOException e) { // TODO Auto-generated catch block @@ -54,13 +55,16 @@ public static void addToArchive(String filePath, String parent, TarArchiveOutput // add tar ArchiveEntry // tarEntry.setMode(755) - tarArchive.putArchiveEntry(new TarArchiveEntry(file, entryName)); + TarArchiveEntry tae = new TarArchiveEntry(file, entryName); + tae.setSize(file.length()); + tarArchive.putArchiveEntry(tae); if(file.isFile()) { -// FileInputStream fis = new FileInputStream(file); -// BufferedInputStream bis = new BufferedInputStream(fis); + FileInputStream fis = new FileInputStream(file); + BufferedInputStream bis = new BufferedInputStream(fis); // // Write file content to archive // IOUtils.copy(bis, tarArchive); - Files.copy(file.toPath(), tarArchive); + org.apache.commons.io.IOUtils.copyLarge(bis, tarArchive); +// Files.copy(file.toPath(), tarArchive); tarArchive.closeArchiveEntry(); // bis.close(); }else if(file.isDirectory()) { diff --git a/common/src/main/scala/org/apache/spark/panda/utils/CompressUtil.scala b/common/src/main/scala/org/apache/spark/panda/utils/CompressUtil.scala index 96ae94b..23e7e47 100644 --- a/common/src/main/scala/org/apache/spark/panda/utils/CompressUtil.scala +++ b/common/src/main/scala/org/apache/spark/panda/utils/CompressUtil.scala @@ -15,7 +15,6 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream * @author fchen */ object CompressUtil { - def zip(sourceDirectory: String, targetZipFile: String): Unit = { val p = Files.createFile(Paths.get(targetZipFile)) val zs = new ZipOutputStream(Files.newOutputStream(p)) @@ -58,12 +57,13 @@ object CompressUtil { case file => val path = file.toPath val tarEntry = new TarArchiveEntry(path.toFile, sourceDirectoryPath.getParent.relativize(path).toString()) - action(path, tarEntry) - taos.putArchiveEntry(tarEntry) -// val in = new FileInputStream(path.toFile) - Files.copy(path, taos) -// IOUtils.copy(in, taos) - taos.closeArchiveEntry() + try { + action(path, tarEntry) + taos.putArchiveEntry(tarEntry) + Files.copy(path, taos) + } finally { + taos.closeArchiveEntry() + } } } catch { case e: Exception => @@ -81,12 +81,6 @@ object CompressUtil { GZIPUtil.createTarArchive(sourceDirectory, targetTarFile) } - def tar3(): Unit = { - Process( - "tar czf " - ) - } - @throws(classOf[IOException]) def unzip(sourceZipFile: String, uncompressedDirectory: String, diff --git a/common/src/main/scala/org/apache/spark/panda/utils/Conda.scala b/common/src/main/scala/org/apache/spark/panda/utils/Conda.scala index d099f32..aceaf72 100644 --- a/common/src/main/scala/org/apache/spark/panda/utils/Conda.scala +++ b/common/src/main/scala/org/apache/spark/panda/utils/Conda.scala @@ -8,6 +8,7 @@ import scala.collection.JavaConverters._ import scala.collection.mutable.Buffer import scala.sys.process.{Process, ProcessLogger} +import org.slf4j.LoggerFactory import org.yaml.snakeyaml.Yaml /** @@ -15,6 +16,13 @@ import org.yaml.snakeyaml.Yaml * @author fchen */ object Conda { + + val logger = LoggerFactory.getLogger(getClass.getCanonicalName) + + private val CONDA_COMMAND = sys.env.getOrElse("CONDA_PATH", "conda") + + logger.info(s"using conda path = ${CONDA_COMMAND}.") + def createEnv(name: String, yaml: JMap[String, Object], basePath: String): Path = { @@ -30,31 +38,39 @@ object Conda { } try { - val cmd = s"conda env create -f ${yamlPath.toString} -p ${envPath}" + val cmd = s"${CONDA_COMMAND} env create -f ${yamlPath.toString} -p ${envPath}" + logger.info(s"running command [ ${cmd} ].") // scalastyle:off println - val logger = ProcessLogger(println, println) + val processLogger = ProcessLogger(logger.info, logger.info) // scalastyle:on Process( cmd - ).!!(logger) + ).!!(processLogger) } catch { case e: RuntimeException => e.printStackTrace() } + logger.info(s"finished create environment $name.") envPath } - def normalize(configurations: String): JMap[String, Object] = { + def normalize(configurations: String, withPyarrow: Boolean = true): JMap[String, Object] = { val yaml = new Yaml() val info = yaml.load[JMap[String, Object]](configurations) val dependencies = info.get("dependencies") + if (withPyarrow) { + // add pyarrow dependency for pyspark runtime. + addPyarrow(dependencies.asInstanceOf[JArrayList[Object]]) + } + val (dep, pip) = extract(dependencies.asInstanceOf[JArrayList[Object]].asScala) + val nname = generateEnvironmentName(dep, pip) + info.put("name", nname) - val total = dep ++ pip + // remove user define channels. + info.remove("channels") - val nname = Util.stringToMD5(total.sortBy(x => x).mkString(",")) - info.put("name", nname) info } @@ -87,6 +103,7 @@ object Conda { } yaml.dump(conf, new FileWriter(filePath.toFile)) + logger.info(s"write [ ${conf.asScala.mkString(",")} ] into [ ${filePath} ] success.") } /** @@ -98,8 +115,36 @@ object Conda { conf } - def addPyarrow(configurations: JMap[String, Object]): Unit = { -// pyarrow==0.12.1 + def addPyarrow(pip: Buffer[String]): Buffer[String] = { + pip.filter(!_.startsWith("pyarrow")) += PYARROW + } + + def addPyarrow(dependencies: JArrayList[Object]): Unit = { + dependencies.asScala + .collect { case map: java.util.LinkedHashMap[_, _] => map} + .headOption + .orElse { + val pip = new java.util.LinkedHashMap[Object, Object]() + pip.put("pip", new JArrayList[Object]()) + dependencies.add(pip) + Option(pip) + }.foreach(pip => { + pip.get("pip") + .asInstanceOf[JArrayList[Object]] + .add(PYARROW) + }) + } + + // todo: (fchen) read from system configurations. + val PYARROW = "pyarrow==0.12.1" + + private def generateEnvironmentName(dependencies: Buffer[String], pip: Buffer[String]): String = { + // We should sort the package first so that we can generate the unique id for the same Conda environment, + // in which the environment dependencies have a shuffled order. + Util.stringToMD5( + dependencies.sortBy(o => o).mkString("dependencies: [", ",", "]") + + pip.sortBy(o => o).mkString("pip: [", ",", "]") + ) } } diff --git a/common/src/main/scala/org/apache/spark/panda/utils/GZIPUtilV2.scala b/common/src/main/scala/org/apache/spark/panda/utils/GZIPUtilV2.scala new file mode 100644 index 0000000..a7dfe75 --- /dev/null +++ b/common/src/main/scala/org/apache/spark/panda/utils/GZIPUtilV2.scala @@ -0,0 +1,109 @@ +package org.apache.spark.panda.utils + +import java.io.{BufferedInputStream, BufferedOutputStream, File, FileInputStream, FileOutputStream, InputStream, IOException} +import java.util.zip.GZIPOutputStream + +import org.apache.commons.compress.archivers.tar.{TarArchiveEntry, TarArchiveOutputStream} +import org.apache.commons.io.IOUtils +import org.apache.commons.logging.LogFactory + +/** + * @time 2019/12/10 上午10:19 + * @author fchen + */ +object GZIPUtilV2 { + + private val logger = LogFactory.getLog(getClass) + + def createTarArchive(parentDir: String, outFile: String): Unit = { + var tarArchive: TarArchiveOutputStream = null + try { + val fos = new FileOutputStream(outFile) + val gzipOS = new GZIPOutputStream(new BufferedOutputStream(fos)) + tarArchive = new TarArchiveOutputStream(gzipOS) + tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) + addToArchive(parentDir, "", tarArchive) + + } finally { + tarArchive.finish() + tarArchive.close() + } + } + + @throws(classOf[IOException]) + def addToArchive(filePath: String, + parent: String, + tarArchive: TarArchiveOutputStream): Unit = { + val file = new File(filePath); + // Create entry name relative to parent file path + // for the archived file + val entryName = parent + file.getName() + // scalastyle:off println + println("entryName " + entryName) + // scalastyle:on + // add tar ArchiveEntry + + // tarEntry.setMode(755) + val tae = new TarArchiveEntry(file, entryName) + tae.setSize(file.length()) + tarArchive.putArchiveEntry(tae) + if (file.isFile()) { + val fis = new FileInputStream(file) + val bis = new BufferedInputStream(fis) + // // Write file content to archive + // IOUtils.copy(bis, tarArchive); + org.apache.commons.io.IOUtils.copyLarge(bis, tarArchive) + // Files.copy(file.toPath(), tarArchive); + tarArchive.closeArchiveEntry() + // bis.close(); + } else if (file.isDirectory()) { + // no content to copy so close archive entry + tarArchive.closeArchiveEntry() + // if this directory contains more directories and files + // traverse and archive them + file.listFiles().foreach(f => addToArchive(f.getAbsolutePath, entryName + File.separator, tarArchive)) + } + } + + def streamCreateTarArchive(outFile: String)(f: TarArchiveOutputStream => Unit): Unit = { + var tarArchive: TarArchiveOutputStream = null + val fos = new FileOutputStream(outFile) + val gzipOS = new GZIPOutputStream(new BufferedOutputStream(fos)) + try { + tarArchive = new TarArchiveOutputStream(gzipOS) + tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) + f(tarArchive) + } finally { + tarArchive.finish() + tarArchive.close() + gzipOS.flush() + gzipOS.close() + fos.flush() + fos.close() + } + } + + @throws(classOf[IOException]) + def streamAddToArchive(in: InputStream, + size: Long, + pathInArchive: String, + tarArchive: TarArchiveOutputStream): Unit = { + + logger.info(s"add entry ${pathInArchive} to archive.") + // add tar ArchiveEntry + + // tarEntry.setMode(755) + val tae = new TarArchiveEntry(pathInArchive) + tae.setSize(size) + tarArchive.putArchiveEntry(tae) + // // Write file content to archive + try { +// IOUtils.copy(in, tarArchive); + org.apache.commons.io.IOUtils.copyLarge(in, tarArchive) + } finally { + // Files.copy(file.toPath(), tarArchive); + tarArchive.closeArchiveEntry() + } + } + +} diff --git a/common/src/main/scala/org/apache/spark/panda/utils/MLFlow.scala b/common/src/main/scala/org/apache/spark/panda/utils/MLFlow.scala new file mode 100644 index 0000000..136e087 --- /dev/null +++ b/common/src/main/scala/org/apache/spark/panda/utils/MLFlow.scala @@ -0,0 +1,99 @@ +package org.apache.spark.panda.utils + +import java.util.{ArrayList => JArrayList, Map => JMap} + +import org.yaml.snakeyaml.Yaml + +/** + * @time 2019-09-16 15:47 + * @author fchen + */ +object MLFlow { + + def env(content: String): Unit = { +// val yaml = new Yaml() +// val mlmodel = yaml.load[JMap[String, Object]](content) +// val flavors = mlmodel.get("flavors").asInstanceOf[JMap[String, Object]] +// println(flavors.get("python_function").getClass) +// println(mlmodel.get("flavors").getClass) + val mlmodel = new MLmodelParser(content) + } + + def main(args: Array[String]): Unit = { + val yaml = + """ + |artifact_path: model + |flavors: + | python_function: + | data: model.pkl + | env: conda.yaml + | loader_module: mlflow.sklearn + | python_version: 3.6.9 + | sklearn: + | pickled_model: model.pkl + | serialization_format: cloudpickle + | sklearn_version: 0.19.1 + |run_id: 9c6c59d0f57f40dfbbded01816896687 + |utc_time_created: '2019-08-21 06:37:27.408296' + """.stripMargin + + val yaml2 = + """ + |flavors: + | python_function: + | artifacts: + | lgb_model: + | path: artifacts/lgb_model_path.pth + | uri: lgb_model_path.pth + | cloudpickle_version: 1.2.1 + | env: conda.yaml + | loader_module: mlflow.pyfunc.model + | python_model: python_model.pkl + | python_version: 3.6.0 + |utc_time_created: '2019-11-20 00:53:01.959671' + |""".stripMargin + + env(yaml) + } + +} + +/** + * a MLmodel file parser. + * here is a MLmodel example: + * -------------------------------------------------------- + * artifact_path: model + * flavors: + * python_function: + * data: model.pkl + * env: conda.yaml + * loader_module: mlflow.sklearn + * python_version: 3.6.9 + * sklearn: + * pickled_model: model.pkl + * serialization_format: cloudpickle + * sklearn_version: 0.19.1 + * run_id: 9c6c59d0f57f40dfbbded01816896687 + * utc_time_created: '2019-08-21 06:37:27.408296' + * -------------------------------------------------------- + * @param content + */ +class MLmodelParser(content: String) { + + val mlmodel = { + new Yaml().load[JMap[String, Object]](content) + } + + val artifactPath = typed[String](mlmodel.getOrDefault("artifact_path", "model")) + + val flavors = typed[JMap[String, Object]](mlmodel.get("flavors")) + + val pythonFunction = typed[JMap[String, Object]](flavors.get("python_function")) + + val env = typed[String](pythonFunction.get("env")) + + def typed[T](obj: Any): T = { + obj.asInstanceOf[T] + } + +} diff --git a/common/src/main/scala/org/apache/spark/panda/utils/MLFlowUtil.scala b/common/src/main/scala/org/apache/spark/panda/utils/MLFlowUtil.scala new file mode 100644 index 0000000..73c2d70 --- /dev/null +++ b/common/src/main/scala/org/apache/spark/panda/utils/MLFlowUtil.scala @@ -0,0 +1,56 @@ +package org.apache.spark.panda.utils + +import org.mlflow.tracking.{MlflowClient, MlflowHttpException} + +/** + * @time 2019/12/9 下午4:24 + * @author fchen + */ +trait MLFlowUtil { + def mlflowTrackingUri: String + protected val client = new MlflowClient(mlflowTrackingUri) + + def artifactUri(runId: String): Either[Throwable, String] = { + try { + Right(client.getRun(runId).getInfo.getArtifactUri) + } catch { +// case e: MlflowHttpException => +// e.printStackTrace() +// None + case t: Throwable => + Left(t) + } +// run.map(_.getInfo.getArtifactUri) + } +} + +object MLFlowUtilTest extends MLFlowUtil { + // http://localhost:5000/api/2.0/mlflow/experiments/list +// override def mlflowTrackingUri: String = "http://localhost:5000" +// override def mlflowTrackingUri: String = "http://192.168.205.91:9999" + override def mlflowTrackingUri: String = "http://mlflow.k8s.uc.host.dxy" + + def main(args: Array[String]): Unit = { +// import scala.collection.JavaConverters._ +// client.listExperiments().asScala.foreach(e => { +// println(e.getName) +// println(client.listRunInfos(e.getExperimentId).size()) +// // client.listArtifacts(e.getName).asScala.foreach(x => { +// // println(x.getPath) +// // }) +// client.listRunInfos(e.getExperimentId).asScala.foreach(x => { +// println(x.getArtifactUri) +// }) +// println(e.getArtifactLocation) +// }) +// +// println("-------") + val run = client.getRun("778825406cf44b02940a617b611c8384") + + run.getInfo + .getArtifactUri +// println(artifactUri("778825406cf44b02940a617b611c8384")) +// client.getExperiment("").getExperiment.get + + } +} diff --git a/common/src/main/scala/org/apache/spark/panda/utils/MinioUtil.scala b/common/src/main/scala/org/apache/spark/panda/utils/MinioUtil.scala new file mode 100644 index 0000000..8b05e0b --- /dev/null +++ b/common/src/main/scala/org/apache/spark/panda/utils/MinioUtil.scala @@ -0,0 +1,119 @@ +package org.apache.spark.panda.utils + +import java.net.{URI, URL} +import java.nio.file.Paths + +import scala.collection.JavaConverters._ + +import io.minio.MinioClient +import io.minio.messages.Item +import org.apache.commons.logging.LogFactory + +/** + * @time 2019/12/9 下午5:22 + * @author fchen + */ +trait MinioUtil { + + val logger = LogFactory.getLog(this.getClass) + + def serverInfo: S3ServerInfo + + val minioClient = + new MinioClient(serverInfo.endpoint, serverInfo.accessKeyId, serverInfo.secretAccessKey) + + def downloadAsZip(bucket: String, + obj: String, + localFilePath: String, + normalizePath: String => String): Unit = { + logger.info(s"begin to download bucket = ${bucket}, object = ${obj}, localFilePath = ${localFilePath}.") + GZIPUtilV2.streamCreateTarArchive(localFilePath) { + tar => { + val f = { + (item: Item) => + val in = minioClient.getObject(bucket, item.objectName()) + try { + GZIPUtilV2.streamAddToArchive(in, item.objectSize(), normalizePath(item.objectName()), tar) + } finally { + in.close() + } + } + visit(bucket, obj, f) + } + } + } + +// def resovleUri(uri: URI): (String, String) = { +// val path = uri.getPath.split("/") +// (path.head, path.slice(1, path.length).mkString("/")) +// } + + /** + * visit given path, bucket/directory. if the bucket doesn't exist, than throw IllegalArgumentException. + * @param bucket the bucket to visit. + * @param obj the object in the bucket to visit. + */ + def visit(bucket: String, + obj: String, + f: Item => Unit): Unit = { + if (minioClient.bucketExists(bucket)) { + minioClient.listObjects(bucket, obj) + .asScala + .map(result => { + val item = result.get() + if (item.isDir) { + visit(bucket, item.objectName(), f) + } else { + f(item) + } + }) + } else { + throw new IllegalArgumentException(s"unknown bucket ${bucket}.") + } + + } + +} + +case class S3ServerInfo( + endpoint: String, + accessKeyId: String, + secretAccessKey: String) + +object MLFlowMinioUtilImpl extends MinioUtil { + override def serverInfo: S3ServerInfo = S3ServerInfo( + sys.env.getOrElse("MLFLOW_S3_ENDPOINT_URL", throw new RuntimeException()), + sys.env.getOrElse("AWS_ACCESS_KEY_ID", throw new RuntimeException()), + sys.env.getOrElse("AWS_SECRET_ACCESS_KEY", throw new RuntimeException()) + ) + + // normalize the sub object path in the root obj. drop the experiment id in the path head. + // e.g. + // /0/a063487ee34e463baf7101d145b96bb7/artifacts => /a063487ee34e463baf7101d145b96bb7/artifacts + val _normalizePath = { + (absolutePathInBucket: String) => + absolutePathInBucket.split("/") match { + case Array(_, _, normalized @ _*) => normalized.mkString("/", "/", "") + } + } + + override def downloadAsZip(bucket: String, + obj: String, + localFilePath: String, + normalizePath: String => String = _normalizePath): Unit = { + super.downloadAsZip(bucket, obj, localFilePath, normalizePath) + } +} + +object MinioUtilTest extends MinioUtil { + override def serverInfo: S3ServerInfo = S3ServerInfo( + "http://192.168.218.59:9000", + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + ) + + def main(args: Array[String]): Unit = { + val a = "s3://test/0/a063487ee34e463baf7101d145b96bb7/artifacts" + val uri = URI.create(a) + } +} diff --git a/common/src/main/scala/org/apache/spark/panda/utils/SFTPUtil.scala b/common/src/main/scala/org/apache/spark/panda/utils/SFTPUtil.scala index a867ccd..e6a6c06 100644 --- a/common/src/main/scala/org/apache/spark/panda/utils/SFTPUtil.scala +++ b/common/src/main/scala/org/apache/spark/panda/utils/SFTPUtil.scala @@ -36,7 +36,13 @@ object SFTPUtil { val config = configs.getConfig(host) val key = ssh.loadKeys(config.getValue("IdentityFile").replaceFirst("~", userHomePath)) val hostname = config.getHostname - val port = config.getPort + val port = { + if (config.getPort != -1) { + config.getPort + } else { + 22 + } + } try { ssh.connect(hostname, port) ssh.authPublickey(config.getUser, key) diff --git a/common/src/main/scala/org/apache/spark/panda/utils/Util.scala b/common/src/main/scala/org/apache/spark/panda/utils/Util.scala index 5051953..fd2781b 100644 --- a/common/src/main/scala/org/apache/spark/panda/utils/Util.scala +++ b/common/src/main/scala/org/apache/spark/panda/utils/Util.scala @@ -1,7 +1,7 @@ package org.apache.spark.panda.utils -import java.io.File -import java.nio.file.{Path, Paths} +import java.io.{File, IOException} +import java.nio.file.{Files, Path, Paths} import java.security.MessageDigest /** @@ -51,6 +51,15 @@ object Util { these ++ these.filter(_.isDirectory).flatMap(recursiveListFiles) } + @throws(classOf[IOException]) + def recursiveDeleteFile(path: String): Unit = { + recursiveListFiles(Paths.get(path).toFile) + .foreach(f => { + Files.deleteIfExists(f.toPath) + }) + Files.deleteIfExists(Paths.get(path)) + } + def stringToMD5(string: String): String = { MessageDigest.getInstance("MD5") .digest(string.getBytes("UTF-8")) @@ -58,5 +67,12 @@ object Util { .mkString } + def mkdir(path: String): Unit = { + val p = Paths.get(path) + if (!Files.exists(p)) { + Files.createDirectories(p) + } + } + } diff --git a/common/src/main/scala/org/panda/Config.scala b/common/src/main/scala/org/panda/Config.scala new file mode 100644 index 0000000..233a037 --- /dev/null +++ b/common/src/main/scala/org/panda/Config.scala @@ -0,0 +1,9 @@ +package org.panda + +/** + * @time 2020/3/9 4:42 下午 + * @author fchen + */ +object Config { + val CACHE_ROOT_DIR = sys.env.getOrElse("BAMBOO_CACHE_DIR", "/tmp/cache") +} diff --git a/common/src/test/scala/org/apache/spark/panda/utils/CondaSuite.scala b/common/src/test/scala/org/apache/spark/panda/utils/CondaSuite.scala new file mode 100644 index 0000000..e377214 --- /dev/null +++ b/common/src/test/scala/org/apache/spark/panda/utils/CondaSuite.scala @@ -0,0 +1,58 @@ +package org.apache.spark.panda.utils + +import org.scalatest.FunSuite + +/** + * @time 2020/1/3 下午1:52 + * @author fchen + */ +class CondaSuite extends FunSuite { + test("basic - the same libary in dependencies and pip should return different environment name.") { + val yaml1 = + """ + |channels: + |- defaults + |dependencies: + |- python=3.7.4 + |- lightgbm==2.2.3 + |- pip: + | - mlflow + | - cloudpickle==1.2.2 + |name: mlflow-env + |""".stripMargin + + val yaml2 = + """ + |channels: + |- defaults + |dependencies: + |- python=3.7.4 + |- pip: + | - mlflow + | - lightgbm==2.2.3 + | - cloudpickle==1.2.2 + |name: mlflow-env + |""".stripMargin + + val name1 = Conda.normalize(yaml1).get("name") + val name2 = Conda.normalize(yaml2).get("name") + assert(name1 != name2) + + // case 2 + val yaml3 = + """ + |dependencies: + |- python=3.7.4 + |""".stripMargin + val yaml4 = + """ + |dependencies: + |- pip: + | - python=3.7.4 + |""".stripMargin + val name3 = Conda.normalize(yaml3, false).get("name") + val name4 = Conda.normalize(yaml4, false).get("name") + assert(name3 != name4) + } + +} diff --git a/core/pom.xml b/core/pom.xml index 1316d35..000dd03 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -30,6 +30,10 @@ org.apache.spark spark-sql_${scala.binary.version} + + org.mlflow + mlflow-client + diff --git a/core/src/main/python/dump_pyfunc.py b/core/src/main/python/dump_pyfunc.py index 2a0327e..43f98c8 100644 --- a/core/src/main/python/dump_pyfunc.py +++ b/core/src/main/python/dump_pyfunc.py @@ -13,6 +13,20 @@ print("function return type: " + str(return_type)) archive_path = sys.argv[3] +class ModelCache(object): + _models = {} + + def __init__(self): + pass + + @staticmethod + def get_or_load(archive_path): + if archive_path in ModelCache._models: + return ModelCache._models[archive_path] + from mlflow.pyfunc import load_pyfunc + ModelCache._models[archive_path] = load_pyfunc(archive_path) + return ModelCache._models[archive_path] + def predict(*args): import pandas from mlflow.pyfunc.spark_model_cache import SparkModelCache @@ -30,11 +44,12 @@ def predict(*args): message="Invalid result_type '{}'. Result type can only be one of or an array of one " "of the following types types: {}".format(str(elem_type), str(supported_types)), error_code=INVALID_PARAMETER_VALUE) - model = SparkModelCache.get_or_load(archive_path) + # model = SparkModelCache.get_or_load(archive_path) # model = load_pyfunc(archive_path) - schema = {str(i): arg for i, arg in enumerate(args)} + model = ModelCache.get_or_load(archive_path) + schema = {series.name: series for i, series in enumerate(args)} # Explicitly pass order of columns to avoid lexicographic ordering (i.e., 10 < 2) - columns = [str(i) for i, _ in enumerate(args)] + columns = [series.name for i, series in enumerate(args)] pdf = pandas.DataFrame(schema, columns=columns) # model.predict(pdf) result = model.predict(pdf) diff --git a/core/src/main/scala/org/apache/spark/catalyst/parser/CreateFunctionParser.scala b/core/src/main/scala/org/apache/spark/catalyst/parser/CreateFunctionParser.scala index 43ff3c3..c0a577d 100644 --- a/core/src/main/scala/org/apache/spark/catalyst/parser/CreateFunctionParser.scala +++ b/core/src/main/scala/org/apache/spark/catalyst/parser/CreateFunctionParser.scala @@ -6,47 +6,49 @@ import scala.collection.JavaConverters._ import org.apache.spark.catalyst.parser.CreateFunctionParser.ExtensionsBuilder import org.apache.spark.sql.{SparkSession, SparkSessionExtensions} -import org.apache.spark.sql.catalyst.parser.{AbstractSqlParser, AstBuilder, ParseException, ParserInterface} +import org.apache.spark.sql.catalyst.parser.{AbstractSqlParser, AstBuilder, ParseException, ParserInterface, SqlBaseParser} import org.apache.spark.sql.catalyst.parser.ParserUtils._ import org.apache.spark.sql.catalyst.parser.SqlBaseParser.{CreateFunctionContext, QualifiedNameContext} import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan -import org.apache.spark.sql.catalyst.trees.Origin +import org.apache.spark.sql.catalyst.rules.ChangePythonUDFStrategy +import org.apache.spark.sql.execution.SparkSqlAstBuilder import org.apache.spark.sql.execution.command.{CreateFunctionCommand, CreateMLFlowFunctionCommand} -import org.apache.spark.sql.internal.SQLConf +import org.apache.spark.sql.execution.python.OneRowRelationToLocalRelation +import org.apache.spark.sql.internal.{SQLConf, VariableSubstitution} /** * @time 2019-09-05 14:00 * @author fchen */ -class CreateFunctionParser() extends AbstractSqlParser { +class CreateFunctionParser(conf: SQLConf) extends AbstractSqlParser { - override def parsePlan(sqlText: String): LogicalPlan = { - parse(sqlText) { parser => - astBuilder.visitSingleStatement(parser.singleStatement()) match { - case plan: LogicalPlan => plan - case _ => - val position = Origin(None, None) - throw new ParseException(Option(sqlText), "Unsupported SQL statement", position, position) - } - } + protected override def parse[T](command: String)(toResult: SqlBaseParser => T): T = { + super.parse(substitutor.substitute(command))(toResult) } - override protected def astBuilder: AstBuilder = new PandaAstBuider(new SQLConf()) + + private val substitutor = new VariableSubstitution(conf) + + override protected def astBuilder: AstBuilder = new PandaAstBuider(conf) } object CreateFunctionParser { type ParserBuilder = (SparkSession, ParserInterface) => ParserInterface type ExtensionsBuilder = SparkSessionExtensions => Unit - val parserBuilder: ParserBuilder = (_, _) => new CreateFunctionParser() + val parserBuilder: ParserBuilder = (_, _) => new CreateFunctionParser(new SQLConf) val extBuilder: ExtensionsBuilder = { e => e.injectParser(parserBuilder)} } class PandaSparkExtensions extends ExtensionsBuilder { override def apply(sessionExtensions: SparkSessionExtensions): Unit = { - sessionExtensions.injectParser((_, _) => new CreateFunctionParser()) + sessionExtensions.injectParser((_, _) => new CreateFunctionParser(new SQLConf)) + sessionExtensions.injectPlannerStrategy(_ => new ChangePythonUDFStrategy) +// sessionExtensions.injectOptimizerRule(_ => new OneRowRelationToLocalRelation) +// sessionExtensions.injectPostHocResolutionRule(_ => new OneRowRelationToLocalRelation) + sessionExtensions.injectResolutionRule(_ => new OneRowRelationToLocalRelation) } } -class PandaAstBuider(conf: SQLConf) extends AstBuilder(conf) { +class PandaAstBuider(conf: SQLConf) extends SparkSqlAstBuilder(conf) { override def visitCreateFunction(ctx: CreateFunctionContext): LogicalPlan = withOrigin(ctx) { val options = ctx.resource.asScala.map { resource => @@ -67,7 +69,7 @@ class PandaAstBuider(conf: SQLConf) extends AstBuilder(conf) { ctx.REPLACE != null) } else { - super.visitCreateFunction(ctx).asInstanceOf[LogicalPlan] + super.visitCreateFunction(ctx) } } } diff --git a/core/src/main/scala/org/apache/spark/sql/catalyst/rules/ChangePythonUDFRule.scala b/core/src/main/scala/org/apache/spark/sql/catalyst/rules/ChangePythonUDFRule.scala new file mode 100644 index 0000000..5cc1316 --- /dev/null +++ b/core/src/main/scala/org/apache/spark/sql/catalyst/rules/ChangePythonUDFRule.scala @@ -0,0 +1,73 @@ +package org.apache.spark.sql.catalyst.rules + +import org.apache.spark.sql.{SparkSession, Strategy} +import org.apache.spark.sql.catalyst.expressions.{Expression, NamedExpression, PythonUDF} +import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project} +import org.apache.spark.sql.execution.{ProjectExec, RDDScanExec, SparkPlan} +import org.apache.spark.sql.execution.python.{ArrowEvalPython, KFArrowEvalPython, KFArrowEvalPythonExec} +import org.apache.spark.sql.types.{DataTypes, StructField, StructType} + +/** + * @time 2020/1/9 1:32 下午 + * @author fchen + */ + +class ChangePythonUDFStrategy extends Strategy { + override def apply(plan: LogicalPlan): Seq[SparkPlan] = { + plan match { + case ArrowEvalPython(udfs, output, child) => + val inputSchema = { + val is = findInputSchema2(udfs) + if (is.size == 0) None else Option(StructType(is.distinct)) + } + val s = findInputSchema(udfs) + KFArrowEvalPythonExec(udfs, inputSchema, output, planLater(child)) :: Nil + case _ => + Nil + } + } + + def findInputSchema(expressions: Seq[Expression]): Option[StructType] = { + if (expressions == null || expressions.size == 0) { + None + } else if (expressions.head.children.forall(_.isInstanceOf[PythonUDF])) { + findInputSchema(expressions.head.children) + } else { + if (expressions.head.children.forall(_.isInstanceOf[NamedExpression])) { + Option(StructType( + expressions.head.children.map(_.asInstanceOf[NamedExpression]).map(ne => { + StructField(ne.name, ne.dataType) + }) + )) + } else { + None + } + } + + Option(StructType( + Seq(StructField("x", DataTypes.IntegerType), + StructField("y", DataTypes.IntegerType) + ) + )) + } + + def findInputSchema2(expressions: Seq[Expression]): Seq[StructField] = { + if (expressions == null || expressions.size == 0) { + Seq.empty + } else { + expressions.flatMap(expression => { + if (expression.children.forall(_.isInstanceOf[PythonUDF])) { + findInputSchema2(expression.children) + } else { + if (expression.children.forall(_.isInstanceOf[NamedExpression])) { + expression.children.map(_.asInstanceOf[NamedExpression]).map(ne => { + StructField(ne.name, ne.dataType) + }) + } else { + Seq.empty[StructField] + } + } + }) + } + } +} diff --git a/core/src/main/scala/org/apache/spark/sql/execution/command/CreateMLFlowFunctionCommand.scala b/core/src/main/scala/org/apache/spark/sql/execution/command/CreateMLFlowFunctionCommand.scala index 1950dc5..a6280f7 100644 --- a/core/src/main/scala/org/apache/spark/sql/execution/command/CreateMLFlowFunctionCommand.scala +++ b/core/src/main/scala/org/apache/spark/sql/execution/command/CreateMLFlowFunctionCommand.scala @@ -1,6 +1,12 @@ package org.apache.spark.sql.execution.command +import java.util.Base64 + +import org.apache.spark.{SparkConf, SparkFiles} +import org.apache.spark.internal.config.ConfigBuilder +import org.apache.spark.panda.utils.{Conda, MLmodelParser} import org.apache.spark.sql.{Row, SparkSession} +import org.apache.spark.sql.internal.SQLConf import org.apache.spark.sql.panda.PandasFunctionManager import org.apache.spark.sql.types.DataType @@ -18,17 +24,90 @@ case class CreateMLFlowFunctionCommand( replace: Boolean) extends RunnableCommand { override def run(sparkSession: SparkSession): Seq[Row] = { - val pythonExec = options.get("pythonexec") - val pythonVer = options.get("pythonver") - PandasFunctionManager.registerMLFlowPythonUDF( - sparkSession, functionName, - returnType = Option(DataType.fromDDL(options("returns"))), - artifactRoot = Option(options("artifactroot")), - runId = className, - driverPythonExec = pythonExec, - driverPythonVer = pythonVer, - pythonExec = pythonExec, - pythonVer = pythonVer) +// val pythonExec = options.get("pythonexec") +// val pythonVer = options.get("pythonver") +// PandasFunctionManager.registerMLFlowPythonUDF( +// sparkSession, functionName, +// returnType = Option(DataType.fromDDL(options("returns"))), +// artifactRoot = Option(options("artifactroot")), +// runId = className, +// driverPythonExec = pythonExec, +// driverPythonVer = pythonVer, +// pythonExec = pythonExec, +// pythonVer = pythonVer) + setup(sparkSession) + Seq.empty[Row] + } + + def setup(sparkSession: SparkSession): Unit = { + + // todo: 下载通过bamboo + + if (CreateMLFlowFunctionCommand.isBambooServerEnable) { + val runid = options.getOrElse("runid", className) + // first we download mlflow run from bamboo server and parser MLmodel file. + val run = s"http://${CreateMLFlowFunctionCommand.bambooServer}/api/v1/artifact/createAndGet/${runid}/${runid}.tgz" + sparkSession.sparkContext.addFile(run) + val mlmodelPath = SparkFiles.get(runid) + s"/artifacts/model/MLmodel" + val content = scala.io.Source.fromFile(mlmodelPath) + .getLines() + .mkString("\n") + val mlmodel = new MLmodelParser(content) + + // second we download the python environment from bamboo server with the conda configurations. + val condaConfPath = SparkFiles.get(runid) + s"/artifacts/${mlmodel.artifactPath}/${mlmodel.env}" + val condaYaml = scala.io.Source.fromFile(condaConfPath) + .getLines() + .mkString("\n") + val name = Conda.normalize(condaYaml).get("name").toString + val encodeConf = Base64.getEncoder.encodeToString(condaYaml.getBytes("utf-8")) + val condaUrl = + s"http://${CreateMLFlowFunctionCommand.bambooServer}/api/v1/conda/createAndGet/${encodeConf}/${name}.tgz" + sparkSession.sparkContext.addFile(condaUrl) + + val driverPython = s"${SparkFiles.get(name)}/bin/python" + val pythonPath = s"./${name}/bin/python" + val pythonExec = Option(pythonPath) + val pythonVer = options.get("pythonver") + + PandasFunctionManager.registerMLFlowPythonUDFLocal( + sparkSession, + functionName, + s"./${runid}/artifacts/${mlmodel.artifactPath}", + returnType = Option(DataType.fromDDL(options("returns"))), + driverPythonExec = Option(driverPython), + driverPythonVer = pythonVer, + pythonExec = pythonExec, + pythonVer = pythonVer) + } else { + val pythonExec = options.get("pythonexec") + PandasFunctionManager.registerMLFlowPythonUDFLocal( + sparkSession, + functionName, + options.getOrElse("modellocalpath", ""), + returnType = Option(DataType.fromDDL(options("returns"))), + driverPythonExec = options.get("driverpythonexec").orElse(pythonExec), + driverPythonVer = options.get("pythonver"), + pythonExec = pythonExec, + pythonVer = options.get("pythonver")) + } Seq.empty[Row] } + +} + +object CreateMLFlowFunctionCommand { + val PANDA_BAMBOO_SERVER_ENABLE = SQLConf.buildConf("spark.panda.bamboo.server.enable") + .booleanConf + .createWithDefault(true) + + val PANDA_BAMBOO_SERVER = SQLConf.buildConf("spark.panda.bamboo.server") + .stringConf + .checkValue(address => !(isBambooServerEnable && address == ""), + "can't find spark.panda.bamboo.server in spark conf, " + + "please make sure you have set right configurations" + ).createWithDefaultString("") + val bambooServer = SQLConf.get.getConf(PANDA_BAMBOO_SERVER) + + val isBambooServerEnable = SQLConf.get.getConf(PANDA_BAMBOO_SERVER_ENABLE) } diff --git a/core/src/main/scala/org/apache/spark/sql/execution/python/KFEvalPythonExec.scala b/core/src/main/scala/org/apache/spark/sql/execution/python/KFEvalPythonExec.scala new file mode 100644 index 0000000..7256874 --- /dev/null +++ b/core/src/main/scala/org/apache/spark/sql/execution/python/KFEvalPythonExec.scala @@ -0,0 +1,187 @@ +package org.apache.spark.sql.execution.python + +import java.io.File + +import scala.collection.JavaConverters._ +import scala.collection.mutable.ArrayBuffer + +import org.apache.spark.{SparkEnv, TaskContext} +import org.apache.spark.api.python.{ChainedPythonFunctions, PythonEvalType} +import org.apache.spark.rdd.RDD +import org.apache.spark.sql.catalyst.InternalRow +import org.apache.spark.sql.catalyst.expressions._ +import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter, LeafNode, LocalRelation, LogicalPlan, OneRowRelation, Project, Statistics, SubqueryAlias, UnaryNode} +import org.apache.spark.sql.catalyst.rules.Rule +import org.apache.spark.sql.execution.{LeafExecNode, SparkPlan} +import org.apache.spark.sql.execution.arrow.ArrowUtils +import org.apache.spark.sql.types.{DataType, StructField, StructType} +import org.apache.spark.util.Utils + +/** + * @time 2020/1/9 1:18 下午 + * @author fchen + * + * copy from spark. + */ +abstract class KFEvalPythonExec(udfs: Seq[PythonUDF], + inputSchema: Option[StructType], + output: Seq[Attribute], + child: SparkPlan) extends EvalPythonExec(udfs, output, child) { + protected override def doExecute(): RDD[InternalRow] = { + val inputRDD = child.execute().map(_.copy()) + + inputRDD.mapPartitions { iter => + val context = TaskContext.get() + + // The queue used to buffer input rows so we can drain it to + // combine input with output from Python. + val queue = HybridRowQueue(context.taskMemoryManager(), + new File(Utils.getLocalDir(SparkEnv.get.conf)), child.output.length) + context.addTaskCompletionListener[Unit] { ctx => + queue.close() + } + + val (pyFuncs, inputs) = udfs.map(collectFunctions).unzip + + // flatten all the arguments + val allInputs = new ArrayBuffer[Expression] + val dataTypes = new ArrayBuffer[DataType] + val argOffsets = inputs.map { input => + input.map { e => + if (allInputs.exists(_.semanticEquals(e))) { + allInputs.indexWhere(_.semanticEquals(e)) + } else { + allInputs += e + dataTypes += e.dataType + allInputs.length - 1 + } + }.toArray + }.toArray + val projection = newMutableProjection(allInputs, child.output) + +// val schema = if (child.schema == null) { +// StructType(dataTypes.zipWithIndex.map { case (dt, i) => +// StructField(s"_$i", dt) +// }) +// } else { +// child.schema +// } + + val schema = inputSchema.getOrElse { + StructType(dataTypes.zipWithIndex.map { case (dt, i) => + StructField(s"_$i", dt) + }) + } + // 为什么这里能拿到正确的output? + schema.printTreeString() + + // Add rows to queue to join later with the result. + val projectedRowIter = iter.map { inputRow => + queue.add(inputRow.asInstanceOf[UnsafeRow]) + projection(inputRow) + } + + val outputRowIterator = evaluate( + pyFuncs, argOffsets, projectedRowIter, schema, context) + + val joined = new JoinedRow + val resultProj = UnsafeProjection.create(output, output) + + outputRowIterator.map { outputRow => + resultProj(joined(queue.remove(), outputRow)) + } + } + } + private def collectFunctions(udf: PythonUDF): (ChainedPythonFunctions, Seq[Expression]) = { + udf.children match { + case Seq(u: PythonUDF) => + val (chained, children) = collectFunctions(u) + (ChainedPythonFunctions(chained.funcs ++ Seq(udf.func)), children) + case children => + // There should not be any other UDFs, or the children can't be evaluated directly. + assert(children.forall(_.find(_.isInstanceOf[PythonUDF]).isEmpty)) + (ChainedPythonFunctions(Seq(udf.func)), udf.children) + } + } + +} + +/** + * A physical plan that evaluates a [[PythonUDF]]. + */ +case class KFArrowEvalPythonExec(udfs: Seq[PythonUDF], + inputSchema: Option[StructType], + output: Seq[Attribute], + child: SparkPlan) + extends KFEvalPythonExec(udfs, inputSchema, output, child) { + + private val batchSize = conf.arrowMaxRecordsPerBatch + private val sessionLocalTimeZone = conf.sessionLocalTimeZone + private val pythonRunnerConf = ArrowUtils.getPythonRunnerConfMap(conf) + + protected override def evaluate(funcs: Seq[ChainedPythonFunctions], + argOffsets: Array[Array[Int]], + iter: Iterator[InternalRow], + schema: StructType, + context: TaskContext): Iterator[InternalRow] = { + + val outputTypes = output.drop(child.output.length).map(_.dataType) + + // DO NOT use iter.grouped(). See BatchIterator. + val batchIter = if (batchSize > 0) new BatchIterator(iter, batchSize) else Iterator(iter) + + val columnarBatchIter = new ArrowPythonRunner( + funcs, + PythonEvalType.SQL_SCALAR_PANDAS_UDF, + argOffsets, + schema, + sessionLocalTimeZone, + pythonRunnerConf).compute(batchIter, context.partitionId(), context) + + new Iterator[InternalRow] { + + private var currentIter = if (columnarBatchIter.hasNext) { + val batch = columnarBatchIter.next() + val actualDataTypes = (0 until batch.numCols()).map(i => batch.column(i).dataType()) + assert(outputTypes == actualDataTypes, "Invalid schema from pandas_udf: " + + s"expected ${outputTypes.mkString(", ")}, got ${actualDataTypes.mkString(", ")}") + batch.rowIterator.asScala + } else { + Iterator.empty + } + + override def hasNext: Boolean = currentIter.hasNext || { + if (columnarBatchIter.hasNext) { + currentIter = columnarBatchIter.next().rowIterator.asScala + hasNext + } else { + false + } + } + + override def next(): InternalRow = currentIter.next() + } + } +} + +case class OneRowRelationToLocalRelation() extends Rule[LogicalPlan] { + override def apply(plan: LogicalPlan): LogicalPlan = { + plan transform { + case proj@ Project(projectList, relation: OneRowRelation) if projectList.forall(_.resolved) => + val output = projectList.map(_.toAttribute) + val data = projectList.map { + case as: Alias => + as.child.eval() + } + Project(projectList, LocalRelation(output, Seq(InternalRow(data)))) + } + } +} + +/** + * A logical plan that evaluates a [[PythonUDF]]. + */ +case class KFArrowEvalPython(udfs: Seq[PythonUDF], + inputSchema: StructType, + output: Seq[Attribute], child: LogicalPlan) + extends UnaryNode diff --git a/core/src/main/scala/org/apache/spark/sql/panda/PandasFunctionManager.scala b/core/src/main/scala/org/apache/spark/sql/panda/PandasFunctionManager.scala index 5e3acd8..d86a4e2 100644 --- a/core/src/main/scala/org/apache/spark/sql/panda/PandasFunctionManager.scala +++ b/core/src/main/scala/org/apache/spark/sql/panda/PandasFunctionManager.scala @@ -38,18 +38,17 @@ object PandasFunctionManager { } def registerMLFlowPythonUDFLocal(spark: SparkSession, - functionName: String, - modelLocalPath: String, - returnType: Option[DataType] = None, - driverPythonExec: Option[String] = None, - driverPythonVer: Option[String] = None, - pythonExec: Option[String] = None, - pythonVer: Option[String] = None - ): Unit = { - val modelPath = SparkModelCache.addLocalModel(spark, modelLocalPath) + functionName: String, + modelLocalPath: String, + returnType: Option[DataType] = None, + driverPythonExec: Option[String] = None, + driverPythonVer: Option[String] = None, + pythonExec: Option[String] = None, + pythonVer: Option[String] = None): Unit = { +// val modelPath = SparkModelCache.addLocalModel(spark, modelLocalPath) val funcSerPath = Utils.createTempDir().getPath + File.separator + "dump_func" writeBinaryPythonFunc( - funcSerPath, modelPath, returnType.getOrElse(IntegerType), + funcSerPath, modelLocalPath, returnType.getOrElse(IntegerType), driverPythonExec.getOrElse("python") ) registerPythonUDF(spark, funcSerPath, functionName, returnType, pythonExec, pythonVer) diff --git a/dev/bamboo/Dockerfile b/dev/bamboo/Dockerfile new file mode 100644 index 0000000..e87ef18 --- /dev/null +++ b/dev/bamboo/Dockerfile @@ -0,0 +1,36 @@ +FROM registry.uc.host.dxy/library/maven-conda + +# Define working directory. +WORKDIR /work + +# Prepare download dependencies +ADD . /work/ + +# setup conda and pip proxy. +RUN echo 'default_channels:\n\ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/msys2/\n\ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/pro/\n\ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/r/\n\ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/free/\n\ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/main/' > ~/.condarc + +RUN mkdir ~/.pip +RUN echo '[global]\n\ +index-url = http://nexus.k8s.uc.host.dxy/repository/pypi-aliyun/simple\n\ +trusted-host=nexus.k8s.uc.host.dxy' > ~/.pip/pip.conf + +ENV LANG C.UTF-8 +ENV MALLOC_ARENA_MAX 4 +ENV CONDA_PATH /opt/conda/bin/conda +ENV MLFLOW_TRACKING_URI http://mlflow.k8s.uc.host.dxy/ + +RUN apt-get update && apt-get install -y git + +# package +RUN mvn -gs settings.xml clean package -DskipTests -Passembly -Dassembly.target.module=kungfu-panda-bamboo -Dassembly.format=dir + +# clear maven cache. +RUN rm -rf ~/.m2 + +# Define default command. +ENTRYPOINT bash kungfu-panda-bin-1.0/kungfu-panda/bootstrap.sh run -Xmx4g -XX:+UseG1GC -Dserver.port=8100 org.panda.bamboo.service.Application diff --git a/dev/maven-conda/Dockerfile b/dev/maven-conda/Dockerfile new file mode 100644 index 0000000..b3264a8 --- /dev/null +++ b/dev/maven-conda/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3.6.3-jdk-8 + +# install miniconda +RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O ~/miniconda.sh && \ + /bin/bash ~/miniconda.sh -b -p /opt/conda && \ + rm ~/miniconda.sh && \ + /opt/conda/bin/conda clean -tipsy && \ + ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \ + echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc && \ + echo "conda activate base" >> ~/.bashrc diff --git a/examples/local/pom.xml b/examples/local/pom.xml index 538f217..7f73e2b 100644 --- a/examples/local/pom.xml +++ b/examples/local/pom.xml @@ -34,6 +34,11 @@ org.apache.spark spark-mllib_${scala.binary.version} + + + + + diff --git a/examples/local/src/main/scala/org/panda/example/local/PandaSqlExample.scala b/examples/local/src/main/scala/org/panda/example/local/PandaSqlExample.scala index 7daad04..e3e1865 100644 --- a/examples/local/src/main/scala/org/panda/example/local/PandaSqlExample.scala +++ b/examples/local/src/main/scala/org/panda/example/local/PandaSqlExample.scala @@ -1,3 +1,4 @@ +// scalastyle:off package org.panda.example.local import java.io.{File, FileInputStream} @@ -6,85 +7,173 @@ import java.util.Base64 import com.google.common.io.ByteStreams import org.apache.spark.catalyst.parser.CreateFunctionParser import org.apache.spark.sql.{SparkSession, SparkSessionExtensions} +import org.apache.spark.sql.panda.PandasFunctionManager +import org.apache.spark.sql.types.StringType /** * @time 2019-09-05 14:59 * @author fchen */ -// scalastyle:off object PandaSqlExample { def main(args: Array[String]): Unit = { // - val path = "/tmp/testdata/12891819633_e4c82b51e8.jpg" - val file = new File(path) - val in = new FileInputStream(file) - val array = ByteStreams.toByteArray(in) - println(array.slice(0, 100).mkString(",")) -// println(new String(Base64.getEncoder.encode(array), "utf-8")) -// println(new String( org.apache.commons.codec.binary.Base64.encodeBase64( -// array -// ))) -//// println(array.slice(0, 100).mkString(",")) -// System.exit(0) - - + test() + System.exit(0) val spark = SparkSession .builder() .appName("panda sql example") .master("local[4]") .config("spark.sql.extensions", "org.apache.spark.catalyst.parser.PandaSparkExtensions") - + .config("spark.panda.bamboo.server.enable", "false") // .withExtensions(CreateFunctionParser.extBuilder) .getOrCreate() + val python = "/usr/local/share/anaconda3/envs/mlflow-study/bin/python" +// val python = "/usr/local/share/anaconda3/envs/pyspark-2.4.3/bin/python" + val path = "/Users/fchen/Project/fchen/kungfu-panda/examples/python/sklearn_kmeans/mlruns/1/e60af958648a4f7981c1195f82d82c1d/artifacts/model" + spark.sql( + s""" + |CREATE FUNCTION `test` AS '909e8c3a8b504f11ac29150af83cee42' USING + | `type` 'mlflow', + | `modelLocalPath` '$path', + | `pythonExec` '$python', + | `returns` 'int' + |""".stripMargin) import spark.implicits._ - import org.apache.spark.sql.functions._ - val binaryFile = spark.sparkContext.binaryFiles("/tmp/testdata/*") - .map(r => (r._1, r._2.toArray())) - .toDF("filename", "image") + Seq( + (11, 22), + (33, 44) + ).toDF("x", "y") + .repartition(1) + .selectExpr("test(x, y)") + .show() +// .explain(true) -// val df = spark.read.format("binaryFile") - val df = binaryFile - .selectExpr("cast(base64(image) as string) as feature", "filename") -// val df = spark.read -// .format("image") -// .load("/tmp/flower_photos/*") -//// .select($"value", input_file_name as "name") -//// .printSchema() -// .select("image.*") -// .selectExpr("base64(data) as feature_array", "origin", "data") -// .selectExpr("cast(feature_array as string) as feature", "xx(feature_array) as f2", "feature_array", "data", "yy(data)") -// df.show() -// System.exit(0) +// spark.sql( +// """ +// |select test(x, y) from ( +// |select 1 as x, 1 as y +// |) +// |""".stripMargin) // .show() -// val python = "/usr/local/share/anaconda3/envs/tensorflow-example/bin/python" - val python = "/usr/local/share/anaconda3/envs/flower_classifier/bin/python" - val artifactRoot = "/tmp" - val runid = "c1f48fc796f3467cb104114f3fa501df" -// val python = "/usr/local/share/anaconda3/envs/mlflow-study/bin/python" -// val artifactRoot = "/Users/fchen/Project/python/mlflow-study/mlruns" -// val runid = "9c6c59d0f57f40dfbbded01816896687" -// + } + + def test(): Unit = { + val spark = SparkSession + .builder() + .appName("panda sql example") + .master("local[4]") + .config("spark.sql.extensions", "org.apache.spark.catalyst.parser.PandaSparkExtensions") + .config("spark.sql.codegen.wholeStage", "false") + .config("spark.sql.execution.arrow.enabled", "true") + .config("spark.panda.bamboo.server.enable", "false") + // .withExtensions(CreateFunctionParser.extBuilder) + .getOrCreate() + val path = "/Users/fchen/Project/fchen/examples/mlflow-in-action/add/mlruns/1/58d234e03699404c938e0ba87d627920/artifacts/model" + val python = "/usr/local/share/anaconda3/envs/mlflow-study/bin/python" + // val python = "/usr/local/share/anaconda3/envs/pyspark-2.4.3/bin/python" +// val path = "/Users/fchen/Project/fchen/kungfu-panda/examples/python/sklearn_kmeans/mlruns/1/e60af958648a4f7981c1195f82d82c1d/artifacts/model" + spark.sql( + s""" + |CREATE FUNCTION `test` AS '909e8c3a8b504f11ac29150af83cee42' USING + | `type` 'mlflow', + | `modelLocalPath` '$path', + | `pythonExec` '$python', + | `returns` 'int' + |""".stripMargin) + + val path2 = "/Users/fchen/Project/fchen/examples/mlflow-in-action/add/mlruns/1/19131276fd084da5b0b629d62448f206/artifacts/model" + // val python = "/usr/local/share/anaconda3/envs/pyspark-2.4.3/bin/python" + // val path = "/Users/fchen/Project/fchen/kungfu-panda/examples/python/sklearn_kmeans/mlruns/1/e60af958648a4f7981c1195f82d82c1d/artifacts/model" spark.sql( s""" - |CREATE FUNCTION `test` AS '${runid}' USING - | `type` 'mlflow', - | `returns` 'array', - | `artifactRoot` '${artifactRoot}', - | `pythonExec` '${python}', - | `pythonVer` '3.7' - """.stripMargin) + |CREATE FUNCTION `test2` AS '909e8c3a8b504f11ac29150af83cee42' USING + | `type` 'mlflow', + | `modelLocalPath` '$path2', + | `pythonExec` '$python', + | `returns` 'int' + |""".stripMargin) - df.selectExpr("test(feature) as predict", "filename").show() +// val dd: Int => Int = (i: Int) => i + 11 +// spark.udf.register("dd", dd) // +// val ff: Int => Int = (i: Int) => i + 13 +// spark.udf.register("ff", ff) + +// spark.sql( +// """ +// |select ff(y) from ( +// |select dd(x) as y from ( +// |select 1223 as x, 13334 +// |)) +// |""".stripMargin) +// .explain(true) + +// val df = spark.sql( +// """ +// |select current_date() +// |""".stripMargin) + + val df = spark.sql( + """ + |select test(x) from ( + |select 5 as x + |) + |""".stripMargin + ) + df.explain(true) + df.show() + +// val df = spark.sql( +// """ +// |select test2(y) from ( +// |select test(x) as y from ( +// |select 1223 as x, 13334 +// |)) +// |""".stripMargin) +// df.explain(true) +// df.show() + println("------------") +// spark.sql("select 1").explain(true) + import spark.implicits._ +// val df = Seq( +// (11, 22), +// (33, 44) +// ).toDF("x", "y") +// .repartition(1) +// .selectExpr("*", "test(x)", "test2(y)", "test(x)") +// df.explain(true) +// df.show +// df.show // spark.sql( // """ -// |select test(x, y) from ( -// |select 1 as x, 1 as y +// |select x + 1 from ( +// |select 1 as x // |) // |""".stripMargin) +// .explain(true) // .show() + } + def badcase: Unit = { + // todo:(fchen) 嵌套下为什么会有问题 + val sql = + """ + |select test(test(x)) from ( + |select 1223 as x, 13334 as y + |) + |""".stripMargin + + val sql2 = + """ + |select *,x from ( + |select 1223 as x, 13334 + |) + |""".stripMargin + val sql3 = + """ + |select test(5) + |""".stripMargin } } diff --git a/examples/python/sklearn_kmeans/README.md b/examples/python/sklearn_kmeans/README.md new file mode 100644 index 0000000..3a0b369 --- /dev/null +++ b/examples/python/sklearn_kmeans/README.md @@ -0,0 +1,30 @@ +# Sklearn KMeans + +1.环境安装 +2.模型训练 +```bash +export MLFLOW_TRACKING_URI="http://192.168.218.172:9999" && /usr/local/share/anaconda3/envs/mlflow-sklearn2/bin/python sklearn_kmeans/new_train.py +``` +3.部署成REST API + +```bash +source activate mlflow-sklearn2 +export MLFLOW_TRACKING_URI="http://192.168.218.172:9999" && mlflow models serve -m runs:/a4dc870e46274ff28fce1a537abd07a0/model --no-conda +``` + +预测 +```bash +curl -X POST \ + http://localhost:5000/invocations \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -H 'postman-token: d26ffff1-3cfd-fab7-114a-8976764b4985' \ + -d '{ + "data": [ + {"x": 1, + "y": 1} + ] +}' +``` + +4. Spark SQL分布式批量预测 diff --git a/examples/python/sklearn_kmeans/conda.yaml b/examples/python/sklearn_kmeans/conda.yaml new file mode 100644 index 0000000..cd9d529 --- /dev/null +++ b/examples/python/sklearn_kmeans/conda.yaml @@ -0,0 +1,18 @@ +name: mlflow-study +channels: + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/main/ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/free/ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/r/ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/pro/ + - http://nexus.k8s.uc.host.dxy/repository/anaconda/pkgs/msys2/ +dependencies: + - python=3.6 + - scikit-learn=0.19.1 + - cloudpickle=0.6.1 + - numpy=1.14.3 + - pandas=0.22.0 + - pyspark=2.4.3 + - matplotlib=3.1.1 + - pip: + - mlflow + - pyarrow==0.12.1 diff --git a/examples/python/sklearn_kmeans/new_train.py b/examples/python/sklearn_kmeans/new_train.py new file mode 100644 index 0000000..fe8faa2 --- /dev/null +++ b/examples/python/sklearn_kmeans/new_train.py @@ -0,0 +1,34 @@ +from pandas import DataFrame + +Data = { + 'x': [25, 34, 22, 27, 33, 33, 31, 22, 35, 34, 67, 54, 57, 43, 50, 57, 59, 52, 65, 47, 49, 48, 35, 33, 44, 45, 38, + 43, 51, 46], + 'y': [79, 51, 53, 78, 59, 74, 73, 57, 69, 75, 51, 32, 40, 47, 53, 36, 35, 58, 59, 50, 25, 20, 14, 12, 20, 5, 29, 27, + 8, 7] + } + +df = DataFrame(Data, columns=['x', 'y']) +# print(df) +from sklearn.cluster import KMeans + +kmeans = KMeans(n_clusters=3).fit(df) + + +d2 = { + 'x': [100], + 'y': [100] +} +d = DataFrame(d2, columns=['x', 'y']) +print(kmeans.predict(d)) + +# import matplotlib.pyplot as plt +# df['cl'] = kmeans.labels_ +# df.plot.scatter('x', 'y', c='cl', colormap='gist_rainbow') +# +# plt.show() + +import mlflow +import mlflow.sklearn +mlflow.set_experiment("odep example") +with mlflow.start_run(): + mlflow.sklearn.log_model(kmeans, "model") diff --git a/examples/yarn/pom.xml b/examples/yarn/pom.xml index 7d39c1e..97afa5a 100644 --- a/examples/yarn/pom.xml +++ b/examples/yarn/pom.xml @@ -36,5 +36,18 @@ + + + single-jar + + + + org.apache.maven.plugins + maven-shade-plugin + + + + + diff --git a/examples/yarn/src/main/scala/org/panda/example/yarn/KmeansExample.scala b/examples/yarn/src/main/scala/org/panda/example/yarn/KmeansExample.scala new file mode 100644 index 0000000..36dbfb0 --- /dev/null +++ b/examples/yarn/src/main/scala/org/panda/example/yarn/KmeansExample.scala @@ -0,0 +1,55 @@ +package org.panda.example.yarn + +import java.io.File + +import org.apache.spark.sql.SparkSession + +/** + * @time 2019/12/10 下午3:52 + * @author fchen + */ +object KmeansExample { + def main(args: Array[String]): Unit = { + val spark = SparkSession + .builder() + .appName("panda sql example") + .master("local[4]") + .config("spark.sql.extensions", "org.apache.spark.catalyst.parser.PandaSparkExtensions") + .config("spark.files.fetchTimeout", "600s") + .config("spark.panda.bamboo.server", "192.168.202.205:8888") +// .config("spark.panda.bamboo.server", "192.168.200.69:8100") + .getOrCreate() + + import spark.implicits._ + + // val python = "/usr/local/share/anaconda3/envs/tensorflow-example/bin/python" + val runid = "a063487ee34e463baf7101d145b96bb7" + // val python = "/usr/local/share/anaconda3/envs/mlflow-study/bin/python" + // val artifactRoot = "/Users/fchen/Project/python/mlflow-study/mlruns" + // val runid = "9c6c59d0f57f40dfbbded01816896687" + // + spark.sql("set spark.sql.crossJoin.enabled = true") + + spark.sql( + s""" + |CREATE FUNCTION `test` AS '${runid}' USING + | `type` 'mlflow', + | `returns` 'int' + """.stripMargin) + + spark.sql( + s""" + |CREATE FUNCTION `test` AS '${runid}' USING + | `type` 'mlflow', + | `returns` 'int' + """.stripMargin) + + spark.sql( + """ + |select test(x, y) from ( + |select 1 as x, 1 as y + |) + |""".stripMargin) + .show() + } +} diff --git a/examples/yarn/src/main/scala/org/panda/example/yarn/Test.scala b/examples/yarn/src/main/scala/org/panda/example/yarn/Test.scala index d7038ca..9cd4301 100644 --- a/examples/yarn/src/main/scala/org/panda/example/yarn/Test.scala +++ b/examples/yarn/src/main/scala/org/panda/example/yarn/Test.scala @@ -35,16 +35,19 @@ // .config(conf) // .getOrCreate() // val python = "/usr/local/share/anaconda3/envs/mlflow-study/bin/python" -// val pythonExec = Option(python) -// PandasFunctionManager.registerMLFlowPythonUDF(spark, functionName = "test", "", -// returnType = Option(IntegerType), pythonExec = pythonExec) -// -// spark.sql( -// """ -// |select test(x, y) from ( -// |select 1 as x, 1 as y -// |) -// |""".stripMargin) -// .show() +// spark.sparkContext.addFile("http://192.168.218.12:8080/api/v1/test/kp") +//// val pythonExec = Option(python) +//// PandasFunctionManager.registerMLFlowPythonUDF(spark, functionName = "test", "", +//// returnType = Option(IntegerType), pythonExec = pythonExec) +//// +//// spark.sql( +//// """ +//// |select test(x, y) from ( +//// |select 1 as x, 1 as y +//// |) +//// |""".stripMargin) +//// .show() +// spark.sql("select 1").show() +// Thread.sleep(Int.MaxValue) // } //} diff --git a/pom.xml b/pom.xml index 6899188..393c7a1 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,19 @@ + + + aliyun + Nexus Release Repository + http://maven.aliyun.com/nexus/content/groups/public + + true + + + true + + + 1.8 @@ -31,6 +44,7 @@ ${project.basedir}/libs 2.4.3 provided + 2.10.3 false @@ -41,6 +55,14 @@ compile + + spark-3.0 + + 3.0.0-preview2 + 2.12 + 2.12.10 + + single-jar @@ -81,6 +103,12 @@ + + assembly + + assembly + + @@ -89,26 +117,26 @@ org.apache.maven.plugins maven-jar-plugin 3.0.2 - - ${build.lib.path} - - - - maven-dependency-plugin - 3.0.1 - - - package - - copy-dependencies - - - provided - ${build.lib.path} - - - + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins @@ -176,31 +204,31 @@ - - org.scalastyle - scalastyle-maven-plugin - 0.8.0 - - false - true - false - false - ${project.basedir}/src/main/scala - ${basedir}/src/test/scala - scalastyle-config.xml - ${basedir}/target/scalastyle-output.xml - ${project.build.sourceEncoding} - ${project.reporting.outputEncoding} - - - - package - - check - - - - + + + + + + + + + + + + + + + + + + + + + + + + + maven-clean-plugin 3.0.0 @@ -272,6 +300,26 @@ ${spark.version} ${spark.scope} + + org.mlflow + mlflow-client + 1.4.0 + + + io.minio + minio + 6.0.11 + + + com.fasterxml.jackson.module + jackson-module-scala_${scala.binary.version} + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + @@ -279,7 +327,6 @@ org.scalatest scalatest_${scala.binary.version} 3.0.3 - test org.scala-lang @@ -293,6 +340,5 @@ bamboo examples/local examples/yarn - assembly diff --git a/settings.xml b/settings.xml new file mode 100755 index 0000000..c3bc7b7 --- /dev/null +++ b/settings.xml @@ -0,0 +1,13 @@ + + + + + dxy + * + dxy + http://nexus.k8s.uc.host.dxy/repository/maven-public/ + + +