From eadaf65ccc7a9f65a31e950c5fb6300b031846b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:40:09 +0000 Subject: [PATCH 01/89] Bump io.netty:netty-all Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.25.Final to 4.1.42.Final. - [Commits](https://github.com/netty/netty/compare/netty-4.1.25.Final...netty-4.1.42.Final) --- updated-dependencies: - dependency-name: io.netty:netty-all dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- samples/dubbo-samples/rpc/dubbo26/dubbo26base/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/dubbo-samples/rpc/dubbo26/dubbo26base/pom.xml b/samples/dubbo-samples/rpc/dubbo26/dubbo26base/pom.xml index e7b8046ac..2845d1578 100644 --- a/samples/dubbo-samples/rpc/dubbo26/dubbo26base/pom.xml +++ b/samples/dubbo-samples/rpc/dubbo26/dubbo26base/pom.xml @@ -76,7 +76,7 @@ io.netty netty-all - 4.1.25.Final + 4.1.42.Final org.springframework.boot From 3a590a37f64523c648b9fe250753973f48f6c994 Mon Sep 17 00:00:00 2001 From: zzm Date: Wed, 13 Dec 2023 17:57:55 +0800 Subject: [PATCH 02/89] symmetric deploy --- .../internal/controller/moduledeployment_controller.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/module-controller/internal/controller/moduledeployment_controller.go b/module-controller/internal/controller/moduledeployment_controller.go index 9361768cb..c032b9781 100644 --- a/module-controller/internal/controller/moduledeployment_controller.go +++ b/module-controller/internal/controller/moduledeployment_controller.go @@ -81,6 +81,16 @@ func (r *ModuleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, utils.Error(err, "Failed to get moduleDeployment", "moduleDeploymentName", moduleDeployment.Name) } + // update symmetric moduleDeployment replicas + if moduleDeployment.Spec.Replicas == -1 { + deployment := &v1.Deployment{} + err := r.Client.Get(ctx, types.NamespacedName{Namespace: moduleDeployment.Namespace, Name: moduleDeployment.Spec.BaseDeploymentName}, deployment) + if err != nil { + return ctrl.Result{}, utils.Error(err, "Failed to get deployment", "deploymentName", deployment.Name) + } + moduleDeployment.Spec.Replicas = deployment.Status.AvailableReplicas + } + if moduleDeployment.DeletionTimestamp != nil { event.PublishModuleDeploymentDeleteEvent(r.Client, ctx, moduleDeployment) if !utils.HasFinalizer(&moduleDeployment.ObjectMeta, finalizer.ModuleReplicaSetExistedFinalizer) && From f5c64e4ee50f0358c7b0a7a1abe33c0a7d5b2833 Mon Sep 17 00:00:00 2001 From: zzm Date: Fri, 15 Dec 2023 10:14:05 +0800 Subject: [PATCH 03/89] symmetric deploy --- ...controller_operation_strategy_suit_test.go | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go b/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go index 886127daf..e1af0f3ae 100644 --- a/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go +++ b/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go @@ -174,6 +174,176 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { }) }) + Context("test symmetric deployment", func() { + namespace := "module-symmetric-deployment-namespace" + namespaceObj := prepareNamespace(namespace) + deployment := prepareDeployment(namespace) + moduleDeploymentName := "module-symmetric-deployment-test" + moduleDeployment := utils.PrepareModuleDeployment(namespace, moduleDeploymentName) + nn := types.NamespacedName{Namespace: namespace, Name: moduleDeploymentName} + // personal params + moduleDeployment.Spec.Replicas = -1 + moduleDeployment.Spec.OperationStrategy.NeedConfirm = false + moduleDeployment.Spec.OperationStrategy.BatchCount = 1 + It("1 prepare namespace", func() { + Eventually(func() error { + err := k8sClient.Create(context.TODO(), &namespaceObj) + if err != nil { + return err + } + + return nil + }, timeout, interval).Should(Succeed()) + }) + + It("2 prepare deployment", func() { + Eventually(func() error { + derr := k8sClient.Create(context.TODO(), &deployment) + if derr != nil { + return derr + } + + // mock + i := int32(3) + deployment.Spec.Replicas = &i + umderr2 := k8sClient.Update(context.TODO(), &deployment) + if umderr2 != nil { + return umderr2 + } + + deployment.Status.Replicas = 3 + deployment.Status.ReadyReplicas = 3 + deployment.Status.AvailableReplicas = 3 + umderr := k8sClient.Status().Update(context.TODO(), &deployment) + if umderr != nil { + return umderr + } + + return nil + }, timeout, interval).Should(Succeed()) + }) + + It("3 prepare moduleDeployment", func() { + Eventually(func() error { + nn := types.NamespacedName{Namespace: namespace, Name: deployment.ObjectMeta.Name} + err2 := k8sClient.Get(context.TODO(), nn, &deployment) + if err2 != nil { + return err2 + } + + mderr := k8sClient.Create(context.TODO(), &moduleDeployment) + if mderr != nil { + return mderr + } + + return nil + }, timeout, interval).Should(Succeed()) + + }) + + It("4 prepare pod 1", func() { + Eventually(func() error { + pod1 := preparePod(namespace, "fake-pod-sym-1") + if err := k8sClient.Create(context.TODO(), &pod1); err != nil { + return err + } + + // when install module, the podIP is necessary + pod1.Status.PodIP = "127.0.0.1" + if perr := k8sClient.Status().Update(context.TODO(), &pod1); perr != nil { + return perr + } + + return nil + }, timeout, interval).Should(Succeed()) + }) + It("5 prepare pod 2", func() { + Eventually(func() error { + pod2 := preparePod(namespace, "fake-pod-sym-2") + if err := k8sClient.Create(context.TODO(), &pod2); err != nil { + return err + } + + // when install module, the podIP is necessary + pod2.Status.PodIP = "127.0.0.1" + if perr := k8sClient.Status().Update(context.TODO(), &pod2); perr != nil { + return perr + } + + return nil + }, timeout, interval).Should(Succeed()) + }) + It("6 prepare pod 3", func() { + Eventually(func() error { + pod3 := preparePod(namespace, "fake-pod-sym-3") + if err := k8sClient.Create(context.TODO(), &pod3); err != nil { + return err + } + + // when install module, the podIP is necessary + pod3.Status.PodIP = "127.0.0.1" + if perr := k8sClient.Status().Update(context.TODO(), &pod3); perr != nil { + return perr + } + + return nil + }, timeout, interval).Should(Succeed()) + }) + + It("7 wait replicaset created", func() { + Eventually(func() bool { + set := map[string]string{label.ModuleDeploymentLabel: moduleDeployment.Name} + replicaSetList := &v1alpha1.ModuleReplicaSetList{} + err := k8sClient.List(context.TODO(), replicaSetList, &client.ListOptions{LabelSelector: labels.SelectorFromSet(set)}, client.InNamespace(moduleDeployment.Namespace)) + if err != nil { + return false + } + + return len(replicaSetList.Items) == 1 + }, timeout, interval).Should(BeTrue()) + }) + + It("8 wait the moduleDeployment is completed", func() { + Eventually(func() bool { + if k8sClient.Get(context.TODO(), nn, &moduleDeployment) != nil { + return false + } + + status := moduleDeployment.Status.ReleaseStatus + if status == nil { + return false + } + + return status.Progress == v1alpha1.ModuleDeploymentReleaseProgressCompleted + }, timeout, interval).Should(BeTrue()) + }) + + It("9 check the moduleDeployment replicas", func() { + Eventually(func() bool { + if err := k8sClient.Get(context.TODO(), nn, &moduleDeployment); err != nil { + return false + } + + return moduleDeployment.Spec.Replicas == 3 + }, timeout, interval).Should(BeTrue()) + }) + + It("10 check replicaSet replicas", func() { + Eventually(func() error { + if err := k8sClient.Get(context.TODO(), nn, &moduleDeployment); err != nil { + return err + } + + return checkModuleDeploymentReplicas(types.NamespacedName{Namespace: moduleDeployment.Namespace, Name: moduleDeploymentName}, 3) + }, timeout, interval).Should(Succeed()) + }) + + It("11 delete moduleDeployment", func() { + Expect(k8sClient.Delete(context.TODO(), &moduleDeployment)).Should(Succeed()) + }) + + }) + Context("test batchConfirm strategy", func() { moduleDeploymentName := "module-deployment-test-for-batch-confirm" nn := types.NamespacedName{Namespace: namespace, Name: moduleDeploymentName} From 47b4ed7e8184c09b3900aa147aa7d5cb5e502eb1 Mon Sep 17 00:00:00 2001 From: zzm Date: Fri, 15 Dec 2023 13:41:34 +0800 Subject: [PATCH 04/89] symmetric deploy --- ...controller_operation_strategy_suit_test.go | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go b/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go index e1af0f3ae..915547eb0 100644 --- a/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go +++ b/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go @@ -225,12 +225,6 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { It("3 prepare moduleDeployment", func() { Eventually(func() error { - nn := types.NamespacedName{Namespace: namespace, Name: deployment.ObjectMeta.Name} - err2 := k8sClient.Get(context.TODO(), nn, &deployment) - if err2 != nil { - return err2 - } - mderr := k8sClient.Create(context.TODO(), &moduleDeployment) if mderr != nil { return mderr @@ -338,7 +332,46 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { }, timeout, interval).Should(Succeed()) }) - It("11 delete moduleDeployment", func() { + }) + + Context("test symmetric deployment err", func() { + namespace := "module-symmetric-deployment-namespace" + moduleDeploymentName := "module-symmetric-deployment-test-2" + moduleDeployment := utils.PrepareModuleDeployment(namespace, moduleDeploymentName) + nn := types.NamespacedName{Namespace: namespace, Name: moduleDeploymentName} + + It("0 prepare moduleDeployment", func() { + Eventually(func() error { + mderr := k8sClient.Create(context.TODO(), &moduleDeployment) + if mderr != nil { + return mderr + } + + return nil + }, timeout, interval).Should(Succeed()) + + }) + + It("1 test symmetric deployment err", func() { + Eventually(func() error { + if err := k8sClient.Get(context.TODO(), nn, &moduleDeployment); err != nil { + return err + } + + moduleDeployment.Spec.BaseDeploymentName = "test-err" + // personal params + moduleDeployment.Spec.Replicas = -1 + moduleDeployment.Spec.OperationStrategy.NeedConfirm = false + moduleDeployment.Spec.OperationStrategy.BatchCount = 1 + if err := k8sClient.Update(context.TODO(), &moduleDeployment); err != nil { + return err + } + + return nil + }, timeout, interval).Should(Succeed()) + }) + + It("2 delete moduleDeployment", func() { Expect(k8sClient.Delete(context.TODO(), &moduleDeployment)).Should(Succeed()) }) From b8309aaeda92e71675645e428412331ff94fdc36 Mon Sep 17 00:00:00 2001 From: zzm Date: Fri, 15 Dec 2023 15:27:05 +0800 Subject: [PATCH 05/89] symmetric deploy ci --- ...build_batch_symmetric_deploy_to_aliyun.yml | 335 ++++++++++++++++++ ...yment_symmetric_batch_deploy_provider.yaml | 28 ++ 2 files changed, 363 insertions(+) create mode 100644 .github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml create mode 100644 module-controller/config/samples/ci/module-deployment_v1alpha1_moduledeployment_symmetric_batch_deploy_provider.yaml diff --git a/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml b/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml new file mode 100644 index 000000000..3a831315f --- /dev/null +++ b/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml @@ -0,0 +1,335 @@ +name: Module Controller Integration Test Symmetric batch deploy +run-name: ${{ github.actor }} pushed module-controller code + +on: + push: + branches: + - master + paths: + - 'module-controller/**' + + pull_request: + branches: + - master + paths: + - 'module-controller/**' + + # enable manually running the workflow + workflow_dispatch: + +env: + CGO_ENABLED: 0 + GOOS: linux + WORK_DIR: module-controller + TAG: ci-test-master-latest + DOCKERHUB_REGISTRY: serverless-registry.cn-shanghai.cr.aliyuncs.com + MODULE_CONTROLLER_IMAGE_PATH: opensource/test/module-controller + INTEGRATION_TESTS_IMAGE_PATH: opensource/test/module-controller-integration-tests + POD_NAMESPACE: default + +defaults: + run: + working-directory: module-controller + +jobs: + unit-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker login + uses: docker/login-action@v2.2.0 + with: + registry: ${{ env.DOCKERHUB_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + logout: false + + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ hashFiles('${{ env.WORK_DIR }}/*Dockerfile') }} + + - name: Build and push module-controller Docker images + uses: docker/build-push-action@v4.1.1 + with: + context: ${{ env.WORK_DIR }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + file: ${{ env.WORK_DIR }}/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.MODULE_CONTROLLER_IMAGE_PATH }}:${{ env.TAG }} + + - run: sleep 30 + + - name: Set up Minikube + run: | + curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 + sudo install minikube-linux-amd64 /usr/local/bin/minikube + + - name: Start Minikube + run: minikube start + + - name: Prepare development env + run: | + kubectl apply -f config/crd/bases/serverless.alipay.com_moduledeployments.yaml + kubectl apply -f config/crd/bases/serverless.alipay.com_modulereplicasets.yaml + kubectl apply -f config/crd/bases/serverless.alipay.com_modules.yaml + kubectl apply -f config/crd/bases/serverless.alipay.com_moduletemplates.yaml + kubectl apply -f config/rbac/role.yaml + kubectl apply -f config/rbac/role_binding.yaml + kubectl apply -f config/rbac/service_account.yaml + kubectl apply -f config/samples/ci/dynamic-stock-batch-deployment.yaml + kubectl apply -f config/samples/ci/module-deployment-controller.yaml + kubectl apply -f config/samples/ci/dynamic-stock-service.yaml + + - run: sleep 60 + + - name: minikube logs + run: minikube logs + + - name: get pod + run: | + kubectl get pod + + - name: wait base pod available + run: | + kubectl wait --for=condition=available deployment/dynamic-stock-deployment --timeout=300s + + - name: wait module controller pod available + run: | + kubectl wait --for=condition=available deployment/module-controller --timeout=300s + + - name: apply moduledeployment symmetric batch release + run: | + kubectl apply -f config/samples/ci/module-deployment_v1alpha1_moduledeployment_symmetric_batch_deploy_provider.yaml + + - name: get moduledeployment + run: | + kubectl get moduledeployment + + - name: get modulereplicaset + run: | + kubectl get modulereplicaset + + - run: sleep 15 + + - name: get module + run: | + kubectl get module -oyaml + + - name: exist module + run: | + moduleCount=$(kubectl get module | wc -l) + if [[ $moduleCount -lt 1 ]]; then + echo "ERROR: 创建首批 module 资源失败" + exit 1 + fi + + - name: wait module available + run: | + # 定义要等待的资源名称和字段值 + modulename=$(kubectl get module -o name) + desired_field_value="Available" + + # 定义等待的超时时间(以秒为单位) + timeout_seconds=300 + + start_time=$(date +%s) + end_time=$((start_time + timeout_seconds)) + + while true; do + current_time=$(date +%s) + if [ $current_time -gt $end_time ]; then + echo "等待超时" + exit 1 + fi + + # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 + field_value=$(kubectl get $modulename -o custom-columns=STATUS:.status.status --no-headers) + + if [ "$field_value" == "$desired_field_value" ]; then + echo "字段值已满足条件" + exit 0 + else + echo "等待字段值满足条件..." + sleep 5 # 等待一段时间后再次检查 + fi + done + + - name: check moduledeployment pause + run: | + # 定义要等待的资源名称和字段值 + moduledeploymentname=$(kubectl get moduledeployment -o name) + desired_field_value=true + + # 定义等待的超时时间(以秒为单位) + timeout_seconds=300 + + start_time=$(date +%s) + end_time=$((start_time + timeout_seconds)) + + while true; do + current_time=$(date +%s) + if [ $current_time -gt $end_time ]; then + echo "等待超时" + exit 1 + fi + + # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 + field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + + if [ "$field_value" == "$desired_field_value" ]; then + echo "字段值已满足条件,执行分组确认" + kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + exit 0 + else + echo "等待字段值满足条件..." + sleep 5 # 等待一段时间后再次检查 + fi + done + + - run: sleep 15 + + - name: get module + run: | + kubectl get module + + - name: exist module more then 1 + run: | + moduleCount=$(kubectl get module | wc -l) + if [[ $moduleCount -lt 2 ]]; then + echo "ERROR: 创建第二批 module 资源失败" + exit 1 + fi + + - name: check moduledeployment pause + run: | + # 定义要等待的资源名称和字段值 + moduledeploymentname=$(kubectl get moduledeployment -o name) + desired_field_value=true + + # 定义等待的超时时间(以秒为单位) + timeout_seconds=300 + + start_time=$(date +%s) + end_time=$((start_time + timeout_seconds)) + + while true; do + current_time=$(date +%s) + if [ $current_time -gt $end_time ]; then + echo "等待超时" + exit 1 + fi + + # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 + field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + + if [ "$field_value" == "$desired_field_value" ]; then + echo "字段值已满足条件,执行分组确认" + kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + exit 0 + else + echo "等待字段值满足条件..." + sleep 5 # 等待一段时间后再次检查 + fi + done + + - name: get module + run: | + kubectl get module + + - name: exist module more then 2 + run: | + moduleCount=$(kubectl get module | wc -l) + if [[ $moduleCount -lt 3 ]]; then + echo "ERROR: 创建第三批 module 资源失败" + exit 1 + fi + - name: wait module available + run: | + # 定义要等待的资源类型和期望的字段值 + moduletype="module" + desired_field_value="Available" + + # 定义等待的超时时间(以秒为单位) + timeout_seconds=300 + + start_time=$(date +%s) + end_time=$((start_time + timeout_seconds)) + + while true; do + current_time=$(date +%s) + if [ $current_time -gt $end_time ]; then + echo "等待超时" + exit 1 + fi + + # 获取所有的资源对象名,并循环处理 + for modulename in $(kubectl get $moduletype -o name); do + # 使用 kubectl get 命令获取每个资源对象的详细信息,并提取自定义字段的值 + field_value=$(kubectl get $modulename -o custom-columns=STATUS:.status.status --no-headers) + + # 检查字段值是否满足期望 + if [ "$field_value" != "$desired_field_value" ]; then + echo "等待字段值满足条件..." + sleep 5 # 等待一段时间后再次检查 + continue 2 # 如果字段值未满足,则跳出循环,进入下一轮等待 + fi + done + + # 如果所有资源对象的字段值都满足期望,则结束脚本 + echo "字段值已满足条件" + exit 0 + done + + - name: batch release successfully then check module install + run: | + label_selector="app=dynamic-stock" + max_attempts=10 + timeout=300 + interval=30 + + for ((i=0; i<$max_attempts; i++)); do + # 获取满足标签选择器条件的所有Pod + podnames=($(kubectl get pods -l $label_selector -o jsonpath='{.items[*].metadata.name}')) + + # 初始化一个变量,用于检测是否所有Pod都满足条件 + all_pods_condition_met=true + + # 遍历所有Pod进行日志检索 + for podname in "${podnames[@]}"; do + log_entry=$(kubectl exec -it $podname -- sh -c 'grep "Install Biz: provider:1.0.2 success" ~/logs/sofa-ark/*.log || true' 2>/dev/null) + + # 如果没有找到日志条目,则将标志设置为false + if [ -z "$log_entry" ]; then + all_pods_condition_met=false + break + fi + done + + # 如果所有Pod都满足条件,则退出循环 + if $all_pods_condition_met; then + echo "所有Pod都满足条件。" + break + fi + + # 如果这不是最后一次尝试,则等待一段时间后继续 + if [ $i -lt $((max_attempts-1)) ]; then + echo "有些Pod未满足条件。等待 $interval 秒后进行下一次尝试。" + sleep $interval + else + # 如果是最后一次尝试,则输出超时消息 + echo "已达到最大尝试次数。有些Pod未满足条件。" + fi + done + + - name: delete deployment + run: | + kubectl delete -n default deployment dynamic-stock-deployment \ No newline at end of file diff --git a/module-controller/config/samples/ci/module-deployment_v1alpha1_moduledeployment_symmetric_batch_deploy_provider.yaml b/module-controller/config/samples/ci/module-deployment_v1alpha1_moduledeployment_symmetric_batch_deploy_provider.yaml new file mode 100644 index 000000000..37c6e9aec --- /dev/null +++ b/module-controller/config/samples/ci/module-deployment_v1alpha1_moduledeployment_symmetric_batch_deploy_provider.yaml @@ -0,0 +1,28 @@ +apiVersion: serverless.alipay.com/v1alpha1 +kind: ModuleDeployment +metadata: + labels: + app.kubernetes.io/name: moduledeployment + app.kubernetes.io/instance: moduledeployment-sample + app.kubernetes.io/part-of: module-controller + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: module-controller + name: moduledeployment-sample-provider +spec: + baseDeploymentName: dynamic-stock-deployment + template: + spec: + module: + name: provider + version: '1.0.2' + url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar + replicas: -1 + operationStrategy: + needConfirm: true + grayTimeBetweenBatchSeconds: 120 + useBeta: false + batchCount: 3 + upgradePolicy: install_then_uninstall + schedulingStrategy: + schedulingPolicy: scatter + From dc87f9e4bcbad3f6ccfdc1bed429b4db00d81ff7 Mon Sep 17 00:00:00 2001 From: qixiaobo Date: Mon, 18 Dec 2023 16:53:20 +0800 Subject: [PATCH 06/89] feat(upload readme): dubbo 2.6.x support --- .../README.md | 1419 +++++++++++++++++ 1 file changed, 1419 insertions(+) create mode 100644 sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md diff --git a/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md b/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md new file mode 100644 index 000000000..4fcbf1729 --- /dev/null +++ b/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md @@ -0,0 +1,1419 @@ +# dubbo 2.6.x support +# 实际使用 +由于目前将Dubbo放入基座 而基座的class目前无法直接增强 +针对当前2.6版本支持需要覆盖对应的dubbo class +因此需要将对应的class复制到classpath中 一般都是按照原封不动的包名放入base模块 确保类加载器优先加载到我们的class从而增强dubbo2.6支持多classLoader +# 背景 +目前我们将Dubbo放入到base基座之后 我们与此同时将biz中的dubbo组件去除 +但是我们发现Dubbo的ExtensionLoader目前是静态类 +其初次加载就已经稳定 这样无法根据classloader不同去不同的biz模块进行加载 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218715966-4e510ce8-4031-4b0e-b5c6-293c4dcfe140.png#averageHue=%230f0f0f&clientId=u8bee2931-a7cd-4&from=paste&height=684&id=uc16f2576&originHeight=684&originWidth=1495&originalType=binary&ratio=1&rotation=0&showTitle=false&size=218206&status=done&style=none&taskId=u0b300a31-daab-4212-8ea9-85f4e5d34f0&title=&width=1495) + +从而导致启动新biz的时候出现报错 +# 思路 +考虑扩充ExtensionLoader当前版本支持不同classloader +这样可以根据ExtensionClassLoader使用不同的BizClassLoader而去重新加载对应资源 +# 改造 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218819626-c5a93c93-3b11-4e7f-a311-87655e085757.png#averageHue=%23646438&clientId=u8bee2931-a7cd-4&from=paste&height=506&id=uf0cd09e8&originHeight=506&originWidth=1422&originalType=binary&ratio=1&rotation=0&showTitle=false&size=171265&status=done&style=none&taskId=u935dc20e-8e5a-4de6-9c8e-dd6aec82457&title=&width=1422) +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218841564-f45a7168-ad17-4f19-b765-7b1b67cf93ee.png#averageHue=%2373633d&clientId=u8bee2931-a7cd-4&from=paste&height=551&id=u9b971db3&originHeight=551&originWidth=1651&originalType=binary&ratio=1&rotation=0&showTitle=false&size=226591&status=done&style=none&taskId=u3dfd5b70-fe18-4dfd-bed8-0f1115c2937&title=&width=1651) + +此时由于biz加载的时候会设置对应的Thread的ContextClassLoader +继而可以触发对应的SPI加载 +``` +package com.alibaba.dubbo.common.extension; + +import com.alibaba.dubbo.common.Constants; +import com.alibaba.dubbo.common.URL; +import com.alibaba.dubbo.common.extension.support.ActivateComparator; +import com.alibaba.dubbo.common.logger.Logger; +import com.alibaba.dubbo.common.logger.LoggerFactory; +import com.alibaba.dubbo.common.utils.ConcurrentHashSet; +import com.alibaba.dubbo.common.utils.ConfigUtils; +import com.alibaba.dubbo.common.utils.Holder; +import com.alibaba.dubbo.common.utils.StringUtils; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +/** + * Load dubbo extensions + *
    + *
  • auto inject dependency extension
  • + *
  • auto wrap extension in wrapper
  • + *
  • default extension is an adaptive instance
  • + *
+ * + * @see Service Provider in Java 5 + * @see com.alibaba.dubbo.common.extension.SPI + * @see com.alibaba.dubbo.common.extension.Adaptive + * @see com.alibaba.dubbo.common.extension.Activate + */ +public class ExtensionLoader { + + private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class); + + private static final String SERVICES_DIRECTORY = "META-INF/services/"; + + private static final String DUBBO_DIRECTORY = "META-INF/dubbo/"; + + private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/"; + + private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*"); + + private static final ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap<>(); + //add by qixiaobo start + private static final ConcurrentMap, ExtensionLoader>> EXTENSION_LOADERS_SUPPORT_CLASSLOADER = new ConcurrentHashMap<>(); + static{ + EXTENSION_LOADERS_SUPPORT_CLASSLOADER.put(findClassLoader(),EXTENSION_LOADERS); + } + //add by qixiaobo end + private static final ConcurrentMap, Object> EXTENSION_INSTANCES = new ConcurrentHashMap, Object>(); + + // ============================== + + private final Class type; + + private final ExtensionFactory objectFactory; + + private final ConcurrentMap, String> cachedNames = new ConcurrentHashMap, String>(); + + private final Holder>> cachedClasses = new Holder>>(); + + private final Map cachedActivates = new ConcurrentHashMap(); + private final ConcurrentMap> cachedInstances = new ConcurrentHashMap>(); + private final Holder cachedAdaptiveInstance = new Holder(); + private volatile Class cachedAdaptiveClass = null; + private String cachedDefaultName; + private volatile Throwable createAdaptiveInstanceError; + + private Set> cachedWrapperClasses; + + private Map exceptions = new ConcurrentHashMap(); + + private ExtensionLoader(Class type) { + this.type = type; + objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); + } + + private static boolean withExtensionAnnotation(Class type) { + return type.isAnnotationPresent(SPI.class); + } + + @SuppressWarnings("unchecked") + public static ExtensionLoader getExtensionLoader(Class type) { + if (type == null) + throw new IllegalArgumentException("Extension type == null"); + if (!type.isInterface()) { + throw new IllegalArgumentException("Extension type(" + type + ") is not interface!"); + } + if (!withExtensionAnnotation(type)) { + throw new IllegalArgumentException("Extension type(" + type + + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!"); + } + ClassLoader classLoader = findClassLoader(); + ConcurrentMap, ExtensionLoader> classExtensionLoaderConcurrentMap = EXTENSION_LOADERS_SUPPORT_CLASSLOADER.get(classLoader); + if (classExtensionLoaderConcurrentMap == null) { + EXTENSION_LOADERS_SUPPORT_CLASSLOADER.putIfAbsent(classLoader, new ConcurrentHashMap<>()); + classExtensionLoaderConcurrentMap = EXTENSION_LOADERS_SUPPORT_CLASSLOADER.get(classLoader); + } +// + ExtensionLoader loader = (ExtensionLoader) classExtensionLoaderConcurrentMap.get(type); + if (loader == null) { + EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); + loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + } + return loader; + } + + private static ClassLoader findClassLoader() { + //add by qixiaobo start support classloader + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if(classLoader!=null) return classLoader; + //add by qixiaobo end + return ExtensionLoader.class.getClassLoader(); + } + + public String getExtensionName(T extensionInstance) { + return getExtensionName(extensionInstance.getClass()); + } + + public String getExtensionName(Class extensionClass) { + return cachedNames.get(extensionClass); + } + + /** + * This is equivalent to {@code getActivateExtension(url, key, null)} + * + * @param url url + * @param key url parameter key which used to get extension point names + * @return extension list which are activated. + * @see #getActivateExtension(com.alibaba.dubbo.common.URL, String, String) + */ + public List getActivateExtension(URL url, String key) { + return getActivateExtension(url, key, null); + } + + /** + * This is equivalent to {@code getActivateExtension(url, values, null)} + * + * @param url url + * @param values extension point names + * @return extension list which are activated + * @see #getActivateExtension(com.alibaba.dubbo.common.URL, String[], String) + */ + public List getActivateExtension(URL url, String[] values) { + return getActivateExtension(url, values, null); + } + + /** + * This is equivalent to {@code getActivateExtension(url, url.getParameter(key).split(","), null)} + * + * @param url url + * @param key url parameter key which used to get extension point names + * @param group group + * @return extension list which are activated. + * @see #getActivateExtension(com.alibaba.dubbo.common.URL, String[], String) + */ + public List getActivateExtension(URL url, String key, String group) { + String value = url.getParameter(key); + return getActivateExtension(url, value == null || value.length() == 0 ? null : Constants.COMMA_SPLIT_PATTERN.split(value), group); + } + + /** + * Get activate extensions. + * + * @param url url + * @param values extension point names + * @param group group + * @return extension list which are activated + * @see com.alibaba.dubbo.common.extension.Activate + */ + public List getActivateExtension(URL url, String[] values, String group) { + List exts = new ArrayList(); + List names = values == null ? new ArrayList(0) : Arrays.asList(values); + if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) { + getExtensionClasses(); + for (Map.Entry entry : cachedActivates.entrySet()) { + String name = entry.getKey(); + Activate activate = entry.getValue(); + if (isMatchGroup(group, activate.group())) { + T ext = getExtension(name); + if (!names.contains(name) + && !names.contains(Constants.REMOVE_VALUE_PREFIX + name) + && isActive(activate, url)) { + exts.add(ext); + } + } + } + Collections.sort(exts, ActivateComparator.COMPARATOR); + } + List usrs = new ArrayList(); + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX) + && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) { + if (Constants.DEFAULT_KEY.equals(name)) { + if (!usrs.isEmpty()) { + exts.addAll(0, usrs); + usrs.clear(); + } + } else { + T ext = getExtension(name); + usrs.add(ext); + } + } + } + if (!usrs.isEmpty()) { + exts.addAll(usrs); + } + return exts; + } + + private boolean isMatchGroup(String group, String[] groups) { + if (group == null || group.length() == 0) { + return true; + } + if (groups != null && groups.length > 0) { + for (String g : groups) { + if (group.equals(g)) { + return true; + } + } + } + return false; + } + + private boolean isActive(Activate activate, URL url) { + String[] keys = activate.value(); + if (keys.length == 0) { + return true; + } + for (String key : keys) { + for (Map.Entry entry : url.getParameters().entrySet()) { + String k = entry.getKey(); + String v = entry.getValue(); + if ((k.equals(key) || k.endsWith("." + key)) + && ConfigUtils.isNotEmpty(v)) { + return true; + } + } + } + return false; + } + + /** + * Get extension's instance. Return null if extension is not found or is not initialized. Pls. note + * that this method will not trigger extension load. + *

+ * In order to trigger extension load, call {@link #getExtension(String)} instead. + * + * @see #getExtension(String) + */ + @SuppressWarnings("unchecked") + public T getLoadedExtension(String name) { + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Extension name == null"); + Holder holder = cachedInstances.get(name); + if (holder == null) { + cachedInstances.putIfAbsent(name, new Holder()); + holder = cachedInstances.get(name); + } + return (T) holder.get(); + } + + /** + * Return the list of extensions which are already loaded. + *

+ * Usually {@link #getSupportedExtensions()} should be called in order to get all extensions. + * + * @see #getSupportedExtensions() + */ + public Set getLoadedExtensions() { + return Collections.unmodifiableSet(new TreeSet(cachedInstances.keySet())); + } + + /** + * Find the extension with the given name. If the specified name is not found, then {@link IllegalStateException} + * will be thrown. + */ + @SuppressWarnings("unchecked") + public T getExtension(String name) { + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Extension name == null"); + if ("true".equals(name)) { + return getDefaultExtension(); + } + Holder holder = cachedInstances.get(name); + if (holder == null) { + cachedInstances.putIfAbsent(name, new Holder()); + holder = cachedInstances.get(name); + } + Object instance = holder.get(); + if (instance == null) { + synchronized (holder) { + instance = holder.get(); + if (instance == null) { + instance = createExtension(name); + holder.set(instance); + } + } + } + return (T) instance; + } + + /** + * Return default extension, return null if it's not configured. + */ + public T getDefaultExtension() { + getExtensionClasses(); + if (null == cachedDefaultName || cachedDefaultName.length() == 0 + || "true".equals(cachedDefaultName)) { + return null; + } + return getExtension(cachedDefaultName); + } + + public boolean hasExtension(String name) { + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Extension name == null"); + try { + this.getExtensionClass(name); + return true; + } catch (Throwable t) { + return false; + } + } + + public Set getSupportedExtensions() { + Map> clazzes = getExtensionClasses(); + return Collections.unmodifiableSet(new TreeSet(clazzes.keySet())); + } + + /** + * Return default extension name, return null if not configured. + */ + public String getDefaultExtensionName() { + getExtensionClasses(); + return cachedDefaultName; + } + + /** + * Register new extension via API + * + * @param name extension name + * @param clazz extension class + * @throws IllegalStateException when extension with the same name has already been registered. + */ + public void addExtension(String name, Class clazz) { + getExtensionClasses(); // load classes + + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Input type " + + clazz + "not implement Extension " + type); + } + if (clazz.isInterface()) { + throw new IllegalStateException("Input type " + + clazz + "can not be interface!"); + } + + if (!clazz.isAnnotationPresent(Adaptive.class)) { + if (StringUtils.isBlank(name)) { + throw new IllegalStateException("Extension name is blank (Extension " + type + ")!"); + } + if (cachedClasses.get().containsKey(name)) { + throw new IllegalStateException("Extension name " + + name + " already existed(Extension " + type + ")!"); + } + + cachedNames.put(clazz, name); + cachedClasses.get().put(name, clazz); + } else { + if (cachedAdaptiveClass != null) { + throw new IllegalStateException("Adaptive Extension already existed(Extension " + type + ")!"); + } + + cachedAdaptiveClass = clazz; + } + } + + /** + * Replace the existing extension via API + * + * @param name extension name + * @param clazz extension class + * @throws IllegalStateException when extension to be placed doesn't exist + * @deprecated not recommended any longer, and use only when test + */ + @Deprecated + public void replaceExtension(String name, Class clazz) { + getExtensionClasses(); // load classes + + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Input type " + + clazz + "not implement Extension " + type); + } + if (clazz.isInterface()) { + throw new IllegalStateException("Input type " + + clazz + "can not be interface!"); + } + + if (!clazz.isAnnotationPresent(Adaptive.class)) { + if (StringUtils.isBlank(name)) { + throw new IllegalStateException("Extension name is blank (Extension " + type + ")!"); + } + if (!cachedClasses.get().containsKey(name)) { + throw new IllegalStateException("Extension name " + + name + " not existed(Extension " + type + ")!"); + } + + cachedNames.put(clazz, name); + cachedClasses.get().put(name, clazz); + cachedInstances.remove(name); + } else { + if (cachedAdaptiveClass == null) { + throw new IllegalStateException("Adaptive Extension not existed(Extension " + type + ")!"); + } + + cachedAdaptiveClass = clazz; + cachedAdaptiveInstance.set(null); + } + } + + @SuppressWarnings("unchecked") + public T getAdaptiveExtension() { + Object instance = cachedAdaptiveInstance.get(); + if (instance == null) { + if (createAdaptiveInstanceError == null) { + synchronized (cachedAdaptiveInstance) { + instance = cachedAdaptiveInstance.get(); + if (instance == null) { + try { + instance = createAdaptiveExtension(); + cachedAdaptiveInstance.set(instance); + } catch (Throwable t) { + createAdaptiveInstanceError = t; + throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t); + } + } + } + } else { + throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); + } + } + + return (T) instance; + } + + private IllegalStateException findException(String name) { + for (Map.Entry entry : exceptions.entrySet()) { + if (entry.getKey().toLowerCase().contains(name.toLowerCase())) { + return entry.getValue(); + } + } + StringBuilder buf = new StringBuilder("No such extension " + type.getName() + " by name " + name); + + + int i = 1; + for (Map.Entry entry : exceptions.entrySet()) { + if (i == 1) { + buf.append(", possible causes: "); + } + + buf.append("\r\n("); + buf.append(i++); + buf.append(") "); + buf.append(entry.getKey()); + buf.append(":\r\n"); + buf.append(StringUtils.toString(entry.getValue())); + } + return new IllegalStateException(buf.toString()); + } + + @SuppressWarnings("unchecked") + private T createExtension(String name) { + Class clazz = getExtensionClasses().get(name); + if (clazz == null) { + throw findException(name); + } + try { + T instance = (T) EXTENSION_INSTANCES.get(clazz); + if (instance == null) { + EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); + instance = (T) EXTENSION_INSTANCES.get(clazz); + } + injectExtension(instance); + Set> wrapperClasses = cachedWrapperClasses; + if (wrapperClasses != null && !wrapperClasses.isEmpty()) { + for (Class wrapperClass : wrapperClasses) { + instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); + } + } + return instance; + } catch (Throwable t) { + throw new IllegalStateException("Extension instance(name: " + name + ", class: " + + type + ") could not be instantiated: " + t.getMessage(), t); + } + } + + private T injectExtension(T instance) { + try { + if (objectFactory != null) { + for (Method method : instance.getClass().getMethods()) { + if (method.getName().startsWith("set") + && method.getParameterTypes().length == 1 + && Modifier.isPublic(method.getModifiers())) { + /** + * Check {@link DisableInject} to see if we need auto injection for this property + */ + if (method.getAnnotation(DisableInject.class) != null) { + continue; + } + Class pt = method.getParameterTypes()[0]; + try { + String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; + Object object = objectFactory.getExtension(pt, property); + if (object != null) { + method.invoke(instance, object); + } + } catch (Exception e) { + logger.error("fail to inject via method " + method.getName() + + " of interface " + type.getName() + ": " + e.getMessage(), e); + } + } + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return instance; + } + + private Class getExtensionClass(String name) { + if (type == null) + throw new IllegalArgumentException("Extension type == null"); + if (name == null) + throw new IllegalArgumentException("Extension name == null"); + Class clazz = getExtensionClasses().get(name); + if (clazz == null) + throw new IllegalStateException("No such extension \"" + name + "\" for " + type.getName() + "!"); + return clazz; + } + + private Map> getExtensionClasses() { + Map> classes = cachedClasses.get(); + if (classes == null) { + synchronized (cachedClasses) { + classes = cachedClasses.get(); + if (classes == null) { + classes = loadExtensionClasses(); + cachedClasses.set(classes); + } + } + } + return classes; + } + + // synchronized in getExtensionClasses + private Map> loadExtensionClasses() { + final SPI defaultAnnotation = type.getAnnotation(SPI.class); + if (defaultAnnotation != null) { + String value = defaultAnnotation.value(); + if ((value = value.trim()).length() > 0) { + String[] names = NAME_SEPARATOR.split(value); + if (names.length > 1) { + throw new IllegalStateException("more than 1 default extension name on extension " + type.getName() + + ": " + Arrays.toString(names)); + } + if (names.length == 1) cachedDefaultName = names[0]; + } + } + + Map> extensionClasses = new HashMap>(); + loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); + loadDirectory(extensionClasses, DUBBO_DIRECTORY); + loadDirectory(extensionClasses, SERVICES_DIRECTORY); + return extensionClasses; + } + + private void loadDirectory(Map> extensionClasses, String dir) { + String fileName = dir + type.getName(); + try { + Enumeration urls; + ClassLoader classLoader = findClassLoader(); + if (classLoader != null) { + urls = classLoader.getResources(fileName); + } else { + urls = ClassLoader.getSystemResources(fileName); + } + if (urls != null) { + while (urls.hasMoreElements()) { + java.net.URL resourceURL = urls.nextElement(); + loadResource(extensionClasses, classLoader, resourceURL); + } + } + } catch (Throwable t) { + logger.error("Exception when load extension class(interface: " + + type + ", description file: " + fileName + ").", t); + } + } + + private void loadResource(Map> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8")); + try { + String line; + while ((line = reader.readLine()) != null) { + final int ci = line.indexOf('#'); + if (ci >= 0) line = line.substring(0, ci); + line = line.trim(); + if (line.length() > 0) { + try { + String name = null; + int i = line.indexOf('='); + if (i > 0) { + name = line.substring(0, i).trim(); + line = line.substring(i + 1).trim(); + } + if (line.length() > 0) { + loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); + } + } catch (Throwable t) { + IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t); + exceptions.put(line, e); + } + } + } + } finally { + reader.close(); + } + } catch (Throwable t) { + logger.error("Exception when load extension class(interface: " + + type + ", class file: " + resourceURL + ") in " + resourceURL, t); + } + } + + private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name) throws NoSuchMethodException { + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Error when load extension class(interface: " + + type + ", class line: " + clazz.getName() + "), class " + + clazz.getName() + "is not subtype of interface."); + } + if (clazz.isAnnotationPresent(Adaptive.class)) { + if (cachedAdaptiveClass == null) { + cachedAdaptiveClass = clazz; + } else if (!cachedAdaptiveClass.equals(clazz)) { + throw new IllegalStateException("More than 1 adaptive class found: " + + cachedAdaptiveClass.getClass().getName() + + ", " + clazz.getClass().getName()); + } + } else if (isWrapperClass(clazz)) { + Set> wrappers = cachedWrapperClasses; + if (wrappers == null) { + cachedWrapperClasses = new ConcurrentHashSet>(); + wrappers = cachedWrapperClasses; + } + wrappers.add(clazz); + } else { + clazz.getConstructor(); + if (name == null || name.length() == 0) { + name = findAnnotationName(clazz); + if (name.length() == 0) { + throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); + } + } + String[] names = NAME_SEPARATOR.split(name); + if (names != null && names.length > 0) { + Activate activate = clazz.getAnnotation(Activate.class); + if (activate != null) { + cachedActivates.put(names[0], activate); + } + for (String n : names) { + if (!cachedNames.containsKey(clazz)) { + cachedNames.put(clazz, n); + } + Class c = extensionClasses.get(n); + if (c == null) { + extensionClasses.put(n, clazz); + } else if (c != clazz) { + throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName()); + } + } + } + } + } + + private boolean isWrapperClass(Class clazz) { + try { + clazz.getConstructor(type); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + @SuppressWarnings("deprecation") + private String findAnnotationName(Class clazz) { + com.alibaba.dubbo.common.Extension extension = clazz.getAnnotation(com.alibaba.dubbo.common.Extension.class); + if (extension == null) { + String name = clazz.getSimpleName(); + if (name.endsWith(type.getSimpleName())) { + name = name.substring(0, name.length() - type.getSimpleName().length()); + } + return name.toLowerCase(); + } + return extension.value(); + } + + @SuppressWarnings("unchecked") + private T createAdaptiveExtension() { + try { + return injectExtension((T) getAdaptiveExtensionClass().newInstance()); + } catch (Exception e) { + throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e); + } + } + + private Class getAdaptiveExtensionClass() { + getExtensionClasses(); + if (cachedAdaptiveClass != null) { + return cachedAdaptiveClass; + } + return cachedAdaptiveClass = createAdaptiveExtensionClass(); + } + + private Class createAdaptiveExtensionClass() { + String code = createAdaptiveExtensionClassCode(); + ClassLoader classLoader = findClassLoader(); + com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); + return compiler.compile(code, classLoader); + } + + private String createAdaptiveExtensionClassCode() { + StringBuilder codeBuilder = new StringBuilder(); + Method[] methods = type.getMethods(); + boolean hasAdaptiveAnnotation = false; + for (Method m : methods) { + if (m.isAnnotationPresent(Adaptive.class)) { + hasAdaptiveAnnotation = true; + break; + } + } + // no need to generate adaptive class since there's no adaptive method found. + if (!hasAdaptiveAnnotation) + throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!"); + + codeBuilder.append("package ").append(type.getPackage().getName()).append(";"); + codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";"); + codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {"); + + for (Method method : methods) { + Class rt = method.getReturnType(); + Class[] pts = method.getParameterTypes(); + Class[] ets = method.getExceptionTypes(); + + Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); + StringBuilder code = new StringBuilder(512); + if (adaptiveAnnotation == null) { + code.append("throw new UnsupportedOperationException(\"method ") + .append(method.toString()).append(" of interface ") + .append(type.getName()).append(" is not adaptive method!\");"); + } else { + int urlTypeIndex = -1; + for (int i = 0; i < pts.length; ++i) { + if (pts[i].equals(URL.class)) { + urlTypeIndex = i; + break; + } + } + // found parameter in URL type + if (urlTypeIndex != -1) { + // Null Point check + String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");", + urlTypeIndex); + code.append(s); + + s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); + code.append(s); + } + // did not find parameter in URL type + else { + String attribMethod = null; + + // find URL getter method + LBL_PTS: + for (int i = 0; i < pts.length; ++i) { + Method[] ms = pts[i].getMethods(); + for (Method m : ms) { + String name = m.getName(); + if ((name.startsWith("get") || name.length() > 3) + && Modifier.isPublic(m.getModifiers()) + && !Modifier.isStatic(m.getModifiers()) + && m.getParameterTypes().length == 0 + && m.getReturnType() == URL.class) { + urlTypeIndex = i; + attribMethod = name; + break LBL_PTS; + } + } + } + if (attribMethod == null) { + throw new IllegalStateException("fail to create adaptive class for interface " + type.getName() + + ": not found url parameter or url attribute in parameters of method " + method.getName()); + } + + // Null point check + String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");", + urlTypeIndex, pts[urlTypeIndex].getName()); + code.append(s); + s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");", + urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod); + code.append(s); + + s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod); + code.append(s); + } + + String[] value = adaptiveAnnotation.value(); + // value is not set, use the value generated from class name as the key + if (value.length == 0) { + char[] charArray = type.getSimpleName().toCharArray(); + StringBuilder sb = new StringBuilder(128); + for (int i = 0; i < charArray.length; i++) { + if (Character.isUpperCase(charArray[i])) { + if (i != 0) { + sb.append("."); + } + sb.append(Character.toLowerCase(charArray[i])); + } else { + sb.append(charArray[i]); + } + } + value = new String[]{sb.toString()}; + } + + boolean hasInvocation = false; + for (int i = 0; i < pts.length; ++i) { + if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) { + // Null Point check + String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i); + code.append(s); + s = String.format("\nString methodName = arg%d.getMethodName();", i); + code.append(s); + hasInvocation = true; + break; + } + } + + String defaultExtName = cachedDefaultName; + String getNameCode = null; + for (int i = value.length - 1; i >= 0; --i) { + if (i == value.length - 1) { + if (null != defaultExtName) { + if (!"protocol".equals(value[i])) + if (hasInvocation) + getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); + else + getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName); + else + getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName); + } else { + if (!"protocol".equals(value[i])) + if (hasInvocation) + getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); + else + getNameCode = String.format("url.getParameter(\"%s\")", value[i]); + else + getNameCode = "url.getProtocol()"; + } + } else { + if (!"protocol".equals(value[i])) + if (hasInvocation) + getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); + else + getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode); + else + getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode); + } + } + code.append("\nString extName = ").append(getNameCode).append(";"); + // check extName == null? + String s = String.format("\nif(extName == null) " + + "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");", + type.getName(), Arrays.toString(value)); + code.append(s); + + s = String.format("\n%s extension = (% 0) { + codeBuilder.append(", "); + } + codeBuilder.append(pts[i].getCanonicalName()); + codeBuilder.append(" "); + codeBuilder.append("arg").append(i); + } + codeBuilder.append(")"); + if (ets.length > 0) { + codeBuilder.append(" throws "); + for (int i = 0; i < ets.length; i++) { + if (i > 0) { + codeBuilder.append(", "); + } + codeBuilder.append(ets[i].getCanonicalName()); + } + } + codeBuilder.append(" {"); + codeBuilder.append(code.toString()); + codeBuilder.append("\n}"); + } + codeBuilder.append("\n}"); + if (logger.isDebugEnabled()) { + logger.debug(codeBuilder.toString()); + } + return codeBuilder.toString(); + } + + @Override + public String toString() { + return this.getClass().getName() + "[" + type.getName() + "]"; + } + +} +``` +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218917861-37b9d42d-b9f8-47df-9046-7edb34c62bc8.png#averageHue=%233b3f42&clientId=u8bee2931-a7cd-4&from=paste&height=425&id=ud6a171c1&originHeight=425&originWidth=508&originalType=binary&ratio=1&rotation=0&showTitle=false&size=42315&status=done&style=none&taskId=u3a538066-041e-4225-b8f3-c4f9002f3b3&title=&width=508) + +dubbo版本2.6.4 +dubbo多biz加载后启动出现一些classloader导致的ClassNotFoundException +``` +2023-10-24 13:38:34,627 [WARN] [NettyServerWorker-9-9] c.a.d.r.p.d.DecodeableRpcInvocation:? [] [DUBBO] Decode argument failed: com.f6car.merchant.so.org.TgOrgGroupMemberSo, dubbo version: 2.6.12, current host: 172.27.121.46 +java.lang.ClassNotFoundException: com.f6car.merchant.so.org.TgOrgGroupMemberSo + at java.net.URLClassLoader.findClass(URLClassLoader.java:387) + at java.lang.ClassLoader.loadClass(ClassLoader.java:418) + at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) + at java.lang.ClassLoader.loadClass(ClassLoader.java:351) + at java.lang.Class.forName0(Native Method) + at java.lang.Class.forName(Class.java:348) + at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:758) + at com.alibaba.dubbo.common.utils.SerialDetector.resolveClass(SerialDetector.java:67) + at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1986) + at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1850) + at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2160) + at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1667) + at java.io.ObjectInputStream.readObject(ObjectInputStream.java:503) + at java.io.ObjectInputStream.readObject(ObjectInputStream.java:461) + at com.alibaba.dubbo.common.serialize.java.JavaObjectInput.readObject(JavaObjectInput.java:70) + at com.alibaba.dubbo.common.serialize.java.JavaObjectInput.readObject(JavaObjectInput.java:77) + at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:122) + at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:72) + at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:138) + at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:126) + at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:86) + at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:46) + at com.alibaba.dubbo.remoting.transport.netty4.NettyCodecAdapter$InternalDecoder.decode(NettyCodecAdapter.java:95) + at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) + at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468) +``` +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698398961430-bb632e4e-d008-4cc4-a4dc-95a634a8ab9b.png#averageHue=%23e6e6e6&clientId=u9afc399f-ea80-4&from=paste&height=417&id=u6286cb3e&originHeight=417&originWidth=866&originalType=binary&ratio=1&rotation=0&showTitle=false&size=118101&status=done&style=none&taskId=u07169dd2-776b-43fb-b490-8098407824c&title=&width=866) +可以看到这个明显是base的classLoader +那么为何dubbo暴露了端口之后进行正反序列化的时候直接使用当前的classLoader呢? +针对正常非biz隔离的场景是OK的 +针对biz存在隔离的case 除非反序列化场景时使用了独立的bizClassLoader +但是问题是我如何知道当前场景下是应该调用哪一个biz呢??? +```java +@Override +public Object decode(Channel channel, InputStream input) throws IOException { + ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType) + .deserialize(channel.getUrl(), input); + this.put(SERIALIZATION_ID_KEY, serializationType); + + String dubboVersion = in.readUTF(); + request.setVersion(dubboVersion); + setAttachment(Constants.DUBBO_VERSION_KEY, dubboVersion); + + String path = in.readUTF(); + setAttachment(Constants.PATH_KEY, path); + String version = in.readUTF(); + setAttachment(Constants.VERSION_KEY, version); + + setMethodName(in.readUTF()); + try { + if (Boolean.parseBoolean(System.getProperty(SERIALIZATION_SECURITY_CHECK_KEY, "false"))) { + CodecSupport.checkSerialization(path, version, serializationType); + } + + Object[] args; + Class[] pts; + String desc = in.readUTF(); + if (desc.length() == 0) { + pts = DubboCodec.EMPTY_CLASS_ARRAY; + args = DubboCodec.EMPTY_OBJECT_ARRAY; + } else { + pts = ReflectUtils.desc2classArray(desc); + args = new Object[pts.length]; + for (int i = 0; i < args.length; i++) { + try { + args[i] = in.readObject(pts[i]); + } catch (Exception e) { + if (log.isWarnEnabled()) { + log.warn("Decode argument failed: " + e.getMessage(), e); + } + } + } + } + setParameterTypes(pts); + + Map map = (Map) in.readObject(Map.class); + if (map != null && map.size() > 0) { + Map attachment = getAttachments(); + if (attachment == null) { + attachment = new HashMap(); + } + attachment.putAll(map); + setAttachments(attachment); + } + //decode argument ,may be callback + for (int i = 0; i < args.length; i++) { + args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]); + } + + setArguments(args); + + } catch (ClassNotFoundException e) { + throw new IOException(StringUtils.toString("Read invocation data failed.", e)); + } finally { + if (in instanceof Cleanable) { + ((Cleanable) in).cleanup(); + } + } + return this; +} +``` +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698399359953-e9745f87-c4cb-4e46-aa79-8ba355aec69f.png#averageHue=%232f2f2e&clientId=u9afc399f-ea80-4&from=paste&height=324&id=uc443f7ac&originHeight=324&originWidth=1226&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64184&status=done&style=none&taskId=u958d86b2-04e1-4409-a75c-9b2158d31e6&title=&width=1226) +也就是在这块需要根据相关的Path自动要解析出对应的Biz 然后根据不同的biz的classLoader进行解析【因此最好在注册rpc的时候记录相关bizClassLoader的关系 后续可以反向使用】 +为了规避此问题目前将dubbo放入biz来进行规避 +不同dubbo的biz会出现端口冲突问题 指定dubbo端口为-1即可 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698399510036-576be94b-6931-4abb-bfcc-f366931b458e.png#averageHue=%23302f2f&clientId=ua7a818cb-964d-4&from=paste&height=373&id=u6ec02f46&originHeight=373&originWidth=1304&originalType=binary&ratio=1&rotation=0&showTitle=false&size=98371&status=done&style=none&taskId=uce2b5b3e-742a-4538-8d96-fbb20342baa&title=&width=1304) + + + +多biz支持后 由于Spring的上下文会绑定到支持Dubbo的SPI的注入 +因此不同biz的容器务必隔离 避免出现bean泄露 +```java +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package com.alibaba.dubbo.config.spring.extension; + +import com.alibaba.dubbo.common.extension.ExtensionFactory; +import com.alibaba.dubbo.common.logger.Logger; +import com.alibaba.dubbo.common.logger.LoggerFactory; +import com.alibaba.dubbo.common.utils.ConcurrentHashSet; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * SpringExtensionFactory + */ +public class SpringExtensionFactory implements ExtensionFactory { + private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class); + + private static final Map> contextsWithClassLoader = new ConcurrentHashMap<>(); + + public static void addApplicationContext(ApplicationContext context) { + getContexts().add(context); + } + + public static void removeApplicationContext(ApplicationContext context) { + getContexts().remove(context); + } + + // currently for test purpose + public static void clearContexts() { + + getContexts().clear(); + } + + @Override + @SuppressWarnings("unchecked") + public T getExtension(Class type, String name) { + for (ApplicationContext context : getContexts()) { + if (context.containsBean(name)) { + Object bean = context.getBean(name); + if (type.isInstance(bean)) { + return (T) bean; + } + } + } + + logger.warn("No spring extension(bean) named:" + name + ", try to find an extension(bean) of type " + type.getName()); + + for (ApplicationContext context : getContexts()) { + try { + return context.getBean(type); + } catch (NoUniqueBeanDefinitionException multiBeanExe) { + throw multiBeanExe; + } catch (NoSuchBeanDefinitionException noBeanExe) { + if (logger.isDebugEnabled()) { + logger.debug("Error when get spring extension(bean) for type:" + type.getName(), noBeanExe); + } + } + } + + logger.warn("No spring extension(bean) named:" + name + ", type:" + type.getName() + " found, stop get bean."); + + return null; + } + + private static ClassLoader findClassLoader() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader != null) return classLoader; + return SpringExtensionFactory.class.getClassLoader(); + } + private static Set getContexts(){ + ClassLoader classLoader = findClassLoader(); + Set contexts = null; + if ((contexts = contextsWithClassLoader.get(classLoader)) == null) { + contextsWithClassLoader.put(classLoader, new ConcurrentHashSet<>()); + contexts = contextsWithClassLoader.get(classLoader); + }; + return contexts; + } + +} + +``` + + +dubbo放入base之后dubbo 由于一部分代码是static执行的 导致部分场景下不同biz的初始化的生命周期不一致 +导致我们部分场景下出现异常 +举例如下 我们的ReferenceConfig在初次加载时会触发 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699266065661-2f31de12-b218-44da-97d9-d6477eaf1f87.png#averageHue=%231f2022&clientId=u898e26e6-1421-4&from=paste&height=498&id=u09ba332b&originHeight=498&originWidth=1359&originalType=binary&ratio=1&rotation=0&showTitle=false&size=115077&status=done&style=none&taskId=u7438f359-df95-43d0-aeb1-4b6064d16d6&title=&width=1359) +这样在不同的biz时由于已经加载完毕后因此无法自动触发导致缺少部分初始化场景 需要补齐 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699266024533-6613a574-3a17-48c1-8009-065ae4520587.png#averageHue=%23232427&clientId=u898e26e6-1421-4&from=paste&height=575&id=udffabb6f&originHeight=575&originWidth=1476&originalType=binary&ratio=1&rotation=0&showTitle=false&size=182369&status=done&style=none&taskId=udd60c607-7e0a-4119-a116-ef52159ab3d&title=&width=1476) + + +目前在同一个端口下 dubbo接收到请求无法判断到当前模块 +已知我们使用的是原生的java序列化方式 +这样自然需要想办法来找到对应的模块 从而取到对应的classLoader +经过sofa社区的尚之同学的建议 给到了如下方案 +```java +/* + * Ant Group + * Copyright (c) 2004-2023 All Rights Reserved. + */ +package com.alibaba.dubbo.demo.provider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; +import java.io.StreamCorruptedException; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; + +import com.alibaba.dubbo.common.URL; +import com.alibaba.dubbo.common.serialize.ObjectInput; +import com.alibaba.dubbo.common.serialize.ObjectOutput; +import com.alibaba.dubbo.common.serialize.java.JavaObjectInput; +import com.alibaba.dubbo.common.serialize.java.JavaObjectOutput; +import com.alibaba.dubbo.common.serialize.java.JavaSerialization; +import com.alibaba.dubbo.config.model.ApplicationModel; +import com.alibaba.dubbo.config.spring.ServiceBean; + +import org.springframework.context.ApplicationContext; + +/** + * + * @author syd + * @version ClassLoaderJavaSerialization.java, v 0.1 2023年10月28日 19:18 syd + */ +public class ClassLoaderJavaSerialization extends JavaSerialization { + + @Override + public byte getContentTypeId() { + return 3; + } + + @Override + public String getContentType() { + return "x-application/java"; + } + + @Override + public ObjectOutput serialize(URL url, OutputStream out) throws IOException { + return new JavaObjectOutput(out); + } + + @Override + public ObjectInput deserialize(URL url, InputStream is) throws IOException { + ClassLoader classLoader = getClassLoaderByUrl(url); + return new JavaObjectInput(new ClassLoaderObjectInputStream(classLoader, is)); + } + + private ClassLoader getClassLoaderByUrl(URL url) { + ServiceBean serviceBean = (ServiceBean) ApplicationModel.getProviderModel(url.getServiceKey()).getMetadata(); + try { + Field field = ServiceBean.class.getField("applicationContext"); + ApplicationContext applicationContext = (ApplicationContext) field.get(serviceBean); + return applicationContext.getClassLoader(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static class ClassLoaderObjectInputStream extends ObjectInputStream { + /** The class loader to use. */ + private final ClassLoader classLoader; + + /** + * Constructs a new ClassLoaderObjectInputStream. + * + * @param classLoader the ClassLoader from which classes should be loaded + * @param inputStream the InputStream to work on + * @throws IOException in case of an I/O error + * @throws StreamCorruptedException if the stream is corrupted + */ + public ClassLoaderObjectInputStream( + ClassLoader classLoader, InputStream inputStream) + throws IOException, StreamCorruptedException { + super(inputStream); + this.classLoader = classLoader; + } + + /** + * Resolve a class specified by the descriptor using the + * specified ClassLoader or the super ClassLoader. + * + * @param objectStreamClass descriptor of the class + * @return the Class object described by the ObjectStreamClass + * @throws IOException in case of an I/O error + * @throws ClassNotFoundException if the Class cannot be found + */ + @Override + protected Class resolveClass(ObjectStreamClass objectStreamClass) + throws IOException, ClassNotFoundException { + + Class clazz = Class.forName(objectStreamClass.getName(), false, classLoader); + + if (clazz != null) { + // the classloader knows of the class + return clazz; + } else { + // classloader knows not of class, let the super classloader do it + return super.resolveClass(objectStreamClass); + } + } + + /** + * Create a proxy class that implements the specified interfaces using + * the specified ClassLoader or the super ClassLoader. + * + * @param interfaces the interfaces to implement + * @return a proxy class implementing the interfaces + * @throws IOException in case of an I/O error + * @throws ClassNotFoundException if the Class cannot be found + * @see java.io.ObjectInputStream#resolveProxyClass(java.lang.String[]) + * @since Commons IO 2.1 + */ + @Override + protected Class resolveProxyClass(String[] interfaces) throws IOException, + ClassNotFoundException { + Class[] interfaceClasses = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + interfaceClasses[i] = Class.forName(interfaces[i], false, classLoader); + } + try { + return Proxy.getProxyClass(classLoader, interfaceClasses); + } catch (IllegalArgumentException e) { + return super.resolveProxyClass(interfaces); + } + } + } +} +``` +我们发现为了使该方案生效 我们必须修改Java序列化 +同时其他不支持多模块的dubbo仍然保持Java的名称 而不是新建新的SPI扩展 +经过研究验证确认我们发现 +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699262773133-6db151d9-abc0-4c61-b4fb-7d1b48a70722.png#averageHue=%23636a61&clientId=u9cade2e4-accb-4&from=paste&height=674&id=u0859502a&originHeight=674&originWidth=1120&originalType=binary&ratio=1&rotation=0&showTitle=false&size=320040&status=done&style=none&taskId=u7354dcef-1b23-4559-83f6-ce08a2c6330&title=&width=1120) +原来url中携带的信息和具体的invocation可能是不匹配的 +究其原因是dubbo会作成dubbo的链接有2类,第一类是共享连接。consumer&每一个provider实例有一个多服务共享的连接。第二类是独享连接,consumer&每一个provider实例的每一个暴露的服务有独立的链接。 +因此我们需要的是要在invocation中获取到对应的path + +因此我们需要复写DecodeableRpcInvocation +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699263362261-24276699-018e-4fe8-8dae-473064b1ef8c.png#averageHue=%2326272a&clientId=u9cade2e4-accb-4&from=paste&height=597&id=ueea820e0&originHeight=597&originWidth=1315&originalType=binary&ratio=1&rotation=0&showTitle=false&size=191220&status=done&style=none&taskId=u9e8da21b-5ced-4187-9dde-5e2ab6113eb&title=&width=1315) +这里有个额外需要注意的点是 +```java +package com.alibaba.dubbo.common.serialize.java; + +import com.alibaba.dubbo.common.serialize.nativejava.NativeJavaObjectInput; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.lang.reflect.Type; + +public class ClassLoaderJavaObjectInput extends NativeJavaObjectInput { + public final static int MAX_BYTE_ARRAY_LENGTH = 8 * 1024 * 1024; + + public ClassLoaderJavaObjectInput(InputStream is) throws IOException { + super((ObjectInputStream)(is instanceof ObjectInputStream ? is : new ObjectInputStream(is))); + } + + @Override + public byte[] readBytes() throws IOException { + int len = getObjectInputStream().readInt(); + if (len < 0) + return null; + if (len == 0) + return new byte[0]; + if (len > MAX_BYTE_ARRAY_LENGTH) + throw new IOException("Byte array length too large. " + len); + + byte[] b = new byte[len]; + getObjectInputStream().readFully(b); + return b; + } + + @Override + public String readUTF() throws IOException { + int len = getObjectInputStream().readInt(); + if (len < 0) + return null; + + return getObjectInputStream().readUTF(); + } + + @Override + public Object readObject() throws IOException, ClassNotFoundException { + byte b = getObjectInputStream().readByte(); + if (b == 0) + return null; + + return getObjectInputStream().readObject(); + } + + @Override + @SuppressWarnings("unchecked") + public T readObject(Class cls) throws IOException, + ClassNotFoundException { + return (T) readObject(); + } + + @Override + @SuppressWarnings("unchecked") + public T readObject(Class cls, Type type) throws IOException, ClassNotFoundException { + return (T) readObject(); + } + + public InputStream getInputStream(){ + return getObjectInputStream(); + } + +} + +``` +![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699263409159-4bc365de-877f-429c-8a81-123d23328a36.png#averageHue=%23202124&clientId=u9cade2e4-accb-4&from=paste&height=161&id=u2ddc141f&originHeight=161&originWidth=1051&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30124&status=done&style=none&taskId=ud5663a6e-def8-4ac0-9d9d-0e3bc8d2f63&title=&width=1051)![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699263420045-b82eded9-7d1a-4757-a6f3-ffb48ed282e9.png#averageHue=%231f2124&clientId=u9cade2e4-accb-4&from=paste&height=296&id=ud1663c18&originHeight=296&originWidth=817&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53230&status=done&style=none&taskId=u6b26ebe9-3404-491f-b7b8-8d2b93e8845&title=&width=817) +注意基类同时支持inputSTream和ObjectInPutSTream 如果不强行转换会走到错误的构造函数 从而导致流被破坏 \ No newline at end of file From 9bf18a9f3a1221ac95eda40616f7596cdf39af7a Mon Sep 17 00:00:00 2001 From: qixiaobo Date: Mon, 18 Dec 2023 17:01:33 +0800 Subject: [PATCH 07/89] feat(upload readme): dubbo 2.6.x support --- .../README.md | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md b/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md index 4fcbf1729..1caa6c511 100644 --- a/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md +++ b/sofa-serverless-runtime/sofa-serverless-adapter-ext/sofa-serverless-adapter-dubbo2.6/README.md @@ -3,22 +3,27 @@ 由于目前将Dubbo放入基座 而基座的class目前无法直接增强 针对当前2.6版本支持需要覆盖对应的dubbo class 因此需要将对应的class复制到classpath中 一般都是按照原封不动的包名放入base模块 确保类加载器优先加载到我们的class从而增强dubbo2.6支持多classLoader + # 背景 目前我们将Dubbo放入到base基座之后 我们与此同时将biz中的dubbo组件去除 但是我们发现Dubbo的ExtensionLoader目前是静态类 其初次加载就已经稳定 这样无法根据classloader不同去不同的biz模块进行加载 + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218715966-4e510ce8-4031-4b0e-b5c6-293c4dcfe140.png#averageHue=%230f0f0f&clientId=u8bee2931-a7cd-4&from=paste&height=684&id=uc16f2576&originHeight=684&originWidth=1495&originalType=binary&ratio=1&rotation=0&showTitle=false&size=218206&status=done&style=none&taskId=u0b300a31-daab-4212-8ea9-85f4e5d34f0&title=&width=1495) 从而导致启动新biz的时候出现报错 + # 思路 考虑扩充ExtensionLoader当前版本支持不同classloader 这样可以根据ExtensionClassLoader使用不同的BizClassLoader而去重新加载对应资源 # 改造 + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218819626-c5a93c93-3b11-4e7f-a311-87655e085757.png#averageHue=%23646438&clientId=u8bee2931-a7cd-4&from=paste&height=506&id=uf0cd09e8&originHeight=506&originWidth=1422&originalType=binary&ratio=1&rotation=0&showTitle=false&size=171265&status=done&style=none&taskId=u935dc20e-8e5a-4de6-9c8e-dd6aec82457&title=&width=1422) ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218841564-f45a7168-ad17-4f19-b765-7b1b67cf93ee.png#averageHue=%2373633d&clientId=u8bee2931-a7cd-4&from=paste&height=551&id=u9b971db3&originHeight=551&originWidth=1651&originalType=binary&ratio=1&rotation=0&showTitle=false&size=226591&status=done&style=none&taskId=u3dfd5b70-fe18-4dfd-bed8-0f1115c2937&title=&width=1651) 此时由于biz加载的时候会设置对应的Thread的ContextClassLoader 继而可以触发对应的SPI加载 + ``` package com.alibaba.dubbo.common.extension; @@ -963,11 +968,13 @@ public class ExtensionLoader { } } + ``` ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698218917861-37b9d42d-b9f8-47df-9046-7edb34c62bc8.png#averageHue=%233b3f42&clientId=u8bee2931-a7cd-4&from=paste&height=425&id=ud6a171c1&originHeight=425&originWidth=508&originalType=binary&ratio=1&rotation=0&showTitle=false&size=42315&status=done&style=none&taskId=u3a538066-041e-4225-b8f3-c4f9002f3b3&title=&width=508) dubbo版本2.6.4 dubbo多biz加载后启动出现一些classloader导致的ClassNotFoundException + ``` 2023-10-24 13:38:34,627 [WARN] [NettyServerWorker-9-9] c.a.d.r.p.d.DecodeableRpcInvocation:? [] [DUBBO] Decode argument failed: com.f6car.merchant.so.org.TgOrgGroupMemberSo, dubbo version: 2.6.12, current host: 172.27.121.46 java.lang.ClassNotFoundException: com.f6car.merchant.so.org.TgOrgGroupMemberSo @@ -997,12 +1004,15 @@ java.lang.ClassNotFoundException: com.f6car.merchant.so.org.TgOrgGroupMemberSo at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468) ``` + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698398961430-bb632e4e-d008-4cc4-a4dc-95a634a8ab9b.png#averageHue=%23e6e6e6&clientId=u9afc399f-ea80-4&from=paste&height=417&id=u6286cb3e&originHeight=417&originWidth=866&originalType=binary&ratio=1&rotation=0&showTitle=false&size=118101&status=done&style=none&taskId=u07169dd2-776b-43fb-b490-8098407824c&title=&width=866) + 可以看到这个明显是base的classLoader 那么为何dubbo暴露了端口之后进行正反序列化的时候直接使用当前的classLoader呢? 针对正常非biz隔离的场景是OK的 针对biz存在隔离的case 除非反序列化场景时使用了独立的bizClassLoader 但是问题是我如何知道当前场景下是应该调用哪一个biz呢??? + ```java @Override public Object decode(Channel channel, InputStream input) throws IOException { @@ -1072,16 +1082,20 @@ public Object decode(Channel channel, InputStream input) throws IOException { return this; } ``` + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698399359953-e9745f87-c4cb-4e46-aa79-8ba355aec69f.png#averageHue=%232f2f2e&clientId=u9afc399f-ea80-4&from=paste&height=324&id=uc443f7ac&originHeight=324&originWidth=1226&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64184&status=done&style=none&taskId=u958d86b2-04e1-4409-a75c-9b2158d31e6&title=&width=1226) + 也就是在这块需要根据相关的Path自动要解析出对应的Biz 然后根据不同的biz的classLoader进行解析【因此最好在注册rpc的时候记录相关bizClassLoader的关系 后续可以反向使用】 为了规避此问题目前将dubbo放入biz来进行规避 不同dubbo的biz会出现端口冲突问题 指定dubbo端口为-1即可 + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1698399510036-576be94b-6931-4abb-bfcc-f366931b458e.png#averageHue=%23302f2f&clientId=ua7a818cb-964d-4&from=paste&height=373&id=u6ec02f46&originHeight=373&originWidth=1304&originalType=binary&ratio=1&rotation=0&showTitle=false&size=98371&status=done&style=none&taskId=uce2b5b3e-742a-4538-8d96-fbb20342baa&title=&width=1304) 多biz支持后 由于Spring的上下文会绑定到支持Dubbo的SPI的注入 因此不同biz的容器务必隔离 避免出现bean泄露 + ```java /* * Licensed to the Apache Software Foundation (ASF) under one or more @@ -1189,8 +1203,11 @@ public class SpringExtensionFactory implements ExtensionFactory { dubbo放入base之后dubbo 由于一部分代码是static执行的 导致部分场景下不同biz的初始化的生命周期不一致 导致我们部分场景下出现异常 举例如下 我们的ReferenceConfig在初次加载时会触发 + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699266065661-2f31de12-b218-44da-97d9-d6477eaf1f87.png#averageHue=%231f2022&clientId=u898e26e6-1421-4&from=paste&height=498&id=u09ba332b&originHeight=498&originWidth=1359&originalType=binary&ratio=1&rotation=0&showTitle=false&size=115077&status=done&style=none&taskId=u7438f359-df95-43d0-aeb1-4b6064d16d6&title=&width=1359) + 这样在不同的biz时由于已经加载完毕后因此无法自动触发导致缺少部分初始化场景 需要补齐 + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699266024533-6613a574-3a17-48c1-8009-065ae4520587.png#averageHue=%23232427&clientId=u898e26e6-1421-4&from=paste&height=575&id=udffabb6f&originHeight=575&originWidth=1476&originalType=binary&ratio=1&rotation=0&showTitle=false&size=182369&status=done&style=none&taskId=udd60c607-7e0a-4119-a116-ef52159ab3d&title=&width=1476) @@ -1198,6 +1215,8 @@ dubbo放入base之后dubbo 由于一部分代码是static执行的 导致部分 已知我们使用的是原生的java序列化方式 这样自然需要想办法来找到对应的模块 从而取到对应的classLoader 经过sofa社区的尚之同学的建议 给到了如下方案 + + ```java /* * Ant Group @@ -1334,17 +1353,23 @@ public class ClassLoaderJavaSerialization extends JavaSerialization { } } ``` + 我们发现为了使该方案生效 我们必须修改Java序列化 同时其他不支持多模块的dubbo仍然保持Java的名称 而不是新建新的SPI扩展 经过研究验证确认我们发现 + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699262773133-6db151d9-abc0-4c61-b4fb-7d1b48a70722.png#averageHue=%23636a61&clientId=u9cade2e4-accb-4&from=paste&height=674&id=u0859502a&originHeight=674&originWidth=1120&originalType=binary&ratio=1&rotation=0&showTitle=false&size=320040&status=done&style=none&taskId=u7354dcef-1b23-4559-83f6-ce08a2c6330&title=&width=1120) + 原来url中携带的信息和具体的invocation可能是不匹配的 究其原因是dubbo会作成dubbo的链接有2类,第一类是共享连接。consumer&每一个provider实例有一个多服务共享的连接。第二类是独享连接,consumer&每一个provider实例的每一个暴露的服务有独立的链接。 因此我们需要的是要在invocation中获取到对应的path 因此我们需要复写DecodeableRpcInvocation + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699263362261-24276699-018e-4fe8-8dae-473064b1ef8c.png#averageHue=%2326272a&clientId=u9cade2e4-accb-4&from=paste&height=597&id=ueea820e0&originHeight=597&originWidth=1315&originalType=binary&ratio=1&rotation=0&showTitle=false&size=191220&status=done&style=none&taskId=u9e8da21b-5ced-4187-9dde-5e2ab6113eb&title=&width=1315) + 这里有个额外需要注意的点是 + ```java package com.alibaba.dubbo.common.serialize.java; @@ -1415,5 +1440,7 @@ public class ClassLoaderJavaObjectInput extends NativeJavaObjectInput { } ``` + ![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699263409159-4bc365de-877f-429c-8a81-123d23328a36.png#averageHue=%23202124&clientId=u9cade2e4-accb-4&from=paste&height=161&id=u2ddc141f&originHeight=161&originWidth=1051&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30124&status=done&style=none&taskId=ud5663a6e-def8-4ac0-9d9d-0e3bc8d2f63&title=&width=1051)![image.png](https://cdn.nlark.com/yuque/0/2023/png/145710/1699263420045-b82eded9-7d1a-4757-a6f3-ffb48ed282e9.png#averageHue=%231f2124&clientId=u9cade2e4-accb-4&from=paste&height=296&id=ud1663c18&originHeight=296&originWidth=817&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53230&status=done&style=none&taskId=u6b26ebe9-3404-491f-b7b8-8d2b93e8845&title=&width=817) -注意基类同时支持inputSTream和ObjectInPutSTream 如果不强行转换会走到错误的构造函数 从而导致流被破坏 \ No newline at end of file + +注意基类同时支持inputStream和ObjectInPutStream 如果不强行转换会走到错误的构造函数 从而导致流被破坏 \ No newline at end of file From 951c284e4986d80ede49ac31e1c1fe49e702a8ee Mon Sep 17 00:00:00 2001 From: lylingzhen <101314559+lylingzhen@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:38:16 +0800 Subject: [PATCH 08/89] Update README.md --- README.md | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 86aa85eb4..f52c822de 100644 --- a/README.md +++ b/README.md @@ -71,28 +71,10 @@ SOFAServerless 技术体系是蚂蚁集团随着业务发展、研发运维的 ## SOFAServerless 开源组件 -| 系统/组件 | 职责描述 | -| ---- | ---- | -| sofa-ark | 原来的 sofa-ark 2.0 Java 合并部署与热部署框架。 | -| arkctl | 研发运维本地工具集,帮助开发者轻松拆分合并模块、开发测试、部署调试、瘦身模块等等。 | -| arklet | 一个客户端框架,对接 SOFAArk 实现 Biz 模块的热部署和热卸载,并且暴露 HTTP API 接口可以让上游系统或者开发者直接使用。 | -| nodejslet | 一个客户端框架,对接 NodeJS 原生 API 实现 NodeJS 模块的热替换,并且暴露 HTTP API 接口可以让上游系统或者开发者直接使用。 | -| module-controller | 模块运维、调度、弹性伸缩系统,清晰的定义 ModuleDeployment 相关模型和 API,并且既支持 K8S CR + ETCD 调和的实现方式又支持标准 HTTP/RPC + DB 的实现方式。底层对接编排模块热替换的客户端(arklet、nodejslet 等)。最终实现模块秒级发布运维能力,让开发者享受 Serverless 发布运维体验。 | +![image](https://github.com/sofastack/sofa-serverless/assets/101314559/995f1e17-f3be-4672-b1b8-c0c041590fb0)
## SOFAServerless 开源版大体里程碑 -SOFAServerless 开源版非常希望并且欢迎与社区同学一起共建,凡是 23 年下半年参与社区共建的同学我们都会颁发奖品。2023 年下半年初步的里程碑计划如下: - -- 2023.8:整体 0.1 版本发布 -- 2023.8:完成 SOFABoot 完整的部署功能验证,产出 benchmark 基线 -- 2023.9:发布基础运维和调度系统 ModuleController 0.5 版 -- 2023.9:发布研发运维工具 Arkctl 与 Arklet 0.5 版 -- 2023.10:发布模块半自动拆分工具 0.5 版 -- 2023.10:编写基础使用教程,新增 2+ SOFABoot 技术栈公司使用 -- 2023.11:支持 SpringBoot 完整能力和 5+ 社区常用中间件 -- 2023.11:SOFAServerless (ModuleController、Arkctl、Arklet、SpringBoot 兼容) 1.0 版本正式发布 -- 2023.12:基础自动弹性伸缩能力发布 - -更具体的里程碑细节随开源项目推进及时更新。 +https://github.com/sofastack/sofa-serverless/releases From 8113003b6d26cf3a9f89e30c32bb4c4f5a66af76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=9E=E5=BB=89?= Date: Tue, 19 Dec 2023 11:12:43 +0800 Subject: [PATCH 09/89] =?UTF-8?q?TYPO=20FIX,=20"=E6=A8=A1=E5=BC=8F"->"?= =?UTF-8?q?=E6=A8=A1=E5=9D=97"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zh-cn/docs/tutorials/module-development/module-debug.md | 2 +- .../docs/tutorials/module-development/module-dev-arkctl.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/zh-cn/docs/tutorials/module-development/module-debug.md b/docs/content/zh-cn/docs/tutorials/module-development/module-debug.md index 31b15f149..059c78e02 100644 --- a/docs/content/zh-cn/docs/tutorials/module-development/module-debug.md +++ b/docs/content/zh-cn/docs/tutorials/module-development/module-debug.md @@ -1,5 +1,5 @@ --- -title: 模式测试 +title: 模块测试 weight: 400 --- diff --git a/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md b/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md index 41905a1c3..20fd3de38 100644 --- a/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md +++ b/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md @@ -1,5 +1,5 @@ --- -title: 模式本地开发 +title: 模块本地开发 weight: 400 --- From 155856d06c206d3cfa20f787d7e8b8f4367ba30b Mon Sep 17 00:00:00 2001 From: leojames Date: Tue, 12 Dec 2023 14:38:24 +0800 Subject: [PATCH 10/89] update ark to 2.2.5 --- .../docs/tutorials/base-create/springboot-and-sofaboot.md | 2 +- .../docs/tutorials/module-development/module-slimming.md | 2 +- docs/public/docs/_print/index.html | 2 +- docs/public/docs/tutorials/_print/index.html | 4 ++-- docs/public/docs/tutorials/base-create/_print/index.html | 2 +- .../base-create/springboot-and-sofaboot/index.html | 4 ++-- .../docs/tutorials/module-development/_print/index.html | 2 +- .../module-development/module-slimming/index.html | 8 ++++---- samples/dubbo-samples/rpc/dubbo26/pom.xml | 2 +- samples/dubbo-samples/rpc/dubbo27/pom.xml | 2 +- samples/dubbo-samples/rpc/dubbo3/pom.xml | 2 +- samples/sofaboot-samples/dynamic-stock/README.md | 2 +- samples/sofaboot-samples/pom.xml | 2 +- samples/springboot-samples/pom.xml | 2 +- .../java/com/alipay/sofa/web/biz1/Biz1Application.java | 3 --- samples/springboot-samples/web/webflux/biz/pom.xml | 2 +- samples/springboot3-samples/db/mybatis/README.md | 2 +- samples/springboot3-samples/logging/README.md | 2 +- samples/springboot3-samples/msg/kafka/README.md | 2 +- samples/springboot3-samples/web/tomcat/README.md | 2 +- sofa-serverless-runtime/pom.xml | 2 +- 21 files changed, 25 insertions(+), 28 deletions(-) diff --git a/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md b/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md index 29f4053a6..337769a29 100644 --- a/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md +++ b/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md @@ -20,7 +20,7 @@ spring.application.name = ${替换为实际基座应用名} #### 修改主 pom.xml ```xml - 2.2.5-SNAPSHOT + 2.2.5 0.5.3 ``` diff --git a/docs/content/zh-cn/docs/tutorials/module-development/module-slimming.md b/docs/content/zh-cn/docs/tutorials/module-development/module-slimming.md index aaa171218..16ac25fc3 100644 --- a/docs/content/zh-cn/docs/tutorials/module-development/module-slimming.md +++ b/docs/content/zh-cn/docs/tutorials/module-development/module-slimming.md @@ -34,7 +34,7 @@ excludeArtifactIds=commons-lang com.alipay.sofa sofa-ark-maven-plugin - 2.2.5-SNAPSHOT + 2.2.5 default-cli diff --git a/docs/public/docs/_print/index.html b/docs/public/docs/_print/index.html index a1b04b53f..414926dd7 100644 --- a/docs/public/docs/_print/index.html +++ b/docs/public/docs/_print/index.html @@ -22,7 +22,7 @@ Your browser does not support the video tag.

该视频的详细文字版教程请点击此处查看。

SOFAServerless 平台和研发框架完整视频教程

步骤 1:点击此处注册开源学堂账号。

步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

4 - 用户手册

4.1 - 基座接入

4.1.1 - SpringBoot 或 SOFABoot 升级为基座

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

代码与配置修改

修改 application.properties

# 需要定义应用名
 spring.application.name = ${替换为实际基座应用名}
 

修改主 pom.xml

<properties>
-    <sofa.ark.verion>2.2.5-SNAPSHOT</sofa.ark.verion>
+    <sofa.ark.verion>2.2.5</sofa.ark.verion>
     <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
 </properties>
 
<dependency>
diff --git a/docs/public/docs/tutorials/_print/index.html b/docs/public/docs/tutorials/_print/index.html
index 5325c6ecd..540f4d128 100644
--- a/docs/public/docs/tutorials/_print/index.html
+++ b/docs/public/docs/tutorials/_print/index.html
@@ -6,7 +6,7 @@
 点击此处打印.

返回本页常规视图.

用户手册

1 - 基座接入

1.1 - SpringBoot 或 SOFABoot 升级为基座

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

代码与配置修改

修改 application.properties

# 需要定义应用名
 spring.application.name = ${替换为实际基座应用名}
 

修改主 pom.xml

<properties>
-    <sofa.ark.verion>2.2.5-SNAPSHOT</sofa.ark.verion>
+    <sofa.ark.verion>2.2.5</sofa.ark.verion>
     <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
 </properties>
 
<dependency>
@@ -204,7 +204,7 @@
     <plugin>
         <groupId>com.alipay.sofa</groupId>
         <artifactId>sofa-ark-maven-plugin</artifactId>
-        <version>2.2.5-SNAPSHOT</version>
+        <version>2.2.5</version>
         <executions>
             <execution>
                 <id>default-cli</id>
diff --git a/docs/public/docs/tutorials/base-create/_print/index.html b/docs/public/docs/tutorials/base-create/_print/index.html
index 773b9f084..db9302085 100644
--- a/docs/public/docs/tutorials/base-create/_print/index.html
+++ b/docs/public/docs/tutorials/base-create/_print/index.html
@@ -6,7 +6,7 @@
 点击此处打印.

返回本页常规视图.

基座接入

1 - SpringBoot 或 SOFABoot 升级为基座

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

代码与配置修改

修改 application.properties

# 需要定义应用名
 spring.application.name = ${替换为实际基座应用名}
 

修改主 pom.xml

<properties>
-    <sofa.ark.verion>2.2.5-SNAPSHOT</sofa.ark.verion>
+    <sofa.ark.verion>2.2.5</sofa.ark.verion>
     <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
 </properties>
 
<dependency>
diff --git a/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html b/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html
index d0c8cc41e..5463cd54d 100644
--- a/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html
+++ b/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html
@@ -1,4 +1,4 @@
-SpringBoot 或 SOFABoot 升级为基座 | SOFAServerless
+SpringBoot 或 SOFABoot 升级为基座 | SOFAServerless
 
 
 
@@ -9,7 +9,7 @@
  整节打印

SpringBoot 或 SOFABoot 升级为基座

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

代码与配置修改

修改 application.properties

# 需要定义应用名
 spring.application.name = ${替换为实际基座应用名}
 

修改主 pom.xml

<properties>
-    <sofa.ark.verion>2.2.5-SNAPSHOT</sofa.ark.verion>
+    <sofa.ark.verion>2.2.5</sofa.ark.verion>
     <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
 </properties>
 
<dependency>
diff --git a/docs/public/docs/tutorials/module-development/_print/index.html b/docs/public/docs/tutorials/module-development/_print/index.html
index 048fafcf5..284b7934e 100644
--- a/docs/public/docs/tutorials/module-development/_print/index.html
+++ b/docs/public/docs/tutorials/module-development/_print/index.html
@@ -9,7 +9,7 @@
     <plugin>
         <groupId>com.alipay.sofa</groupId>
         <artifactId>sofa-ark-maven-plugin</artifactId>
-        <version>2.2.5-SNAPSHOT</version>
+        <version>2.2.5</version>
         <executions>
             <execution>
                 <id>default-cli</id>
diff --git a/docs/public/docs/tutorials/module-development/module-slimming/index.html b/docs/public/docs/tutorials/module-development/module-slimming/index.html
index ba6d195e0..a9257e1b7 100644
--- a/docs/public/docs/tutorials/module-development/module-slimming/index.html
+++ b/docs/public/docs/tutorials/module-development/module-slimming/index.html
@@ -6,15 +6,15 @@
 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。 一键自动瘦身 瘦身原则 构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。
 步骤一 在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。
 excludeGroupIds=org.apache* excludeArtifactIds=commons-lang 步骤二 在模块打包插件中,引入上述配置文件:
-  com.alipay.sofa sofa-ark-maven-plugin 2.2.5-SNAPSHOT   default-cli  repackage     true ./target biz1   rules.txt biz1 true  
+  com.alipay.sofa sofa-ark-maven-plugin 2.2.5   default-cli  repackage     true ./target biz1   rules.txt biz1 true  
 
 
 
@@ -28,7 +28,7 @@
     <plugin>
         <groupId>com.alipay.sofa</groupId>
         <artifactId>sofa-ark-maven-plugin</artifactId>
-        <version>2.2.5-SNAPSHOT</version>
+        <version>2.2.5</version>
         <executions>
             <execution>
                 <id>default-cli</id>
diff --git a/samples/dubbo-samples/rpc/dubbo26/pom.xml b/samples/dubbo-samples/rpc/dubbo26/pom.xml
index b0ff45ddb..a298434b5 100644
--- a/samples/dubbo-samples/rpc/dubbo26/pom.xml
+++ b/samples/dubbo-samples/rpc/dubbo26/pom.xml
@@ -19,7 +19,7 @@
 		8
 		2.6.12
 		2.7.16
-		2.2.5-SNAPSHOT
+		2.2.5
 		0.5.3
 		3.4.2
 		3.8.1
diff --git a/samples/dubbo-samples/rpc/dubbo27/pom.xml b/samples/dubbo-samples/rpc/dubbo27/pom.xml
index 88fd0ecff..b6b3fadd9 100644
--- a/samples/dubbo-samples/rpc/dubbo27/pom.xml
+++ b/samples/dubbo-samples/rpc/dubbo27/pom.xml
@@ -19,7 +19,7 @@
 		8
 		2.7.23
 		2.7.16
-		2.2.5-SNAPSHOT
+		2.2.5
 		0.5.0
 		3.4.2
 		3.8.1
diff --git a/samples/dubbo-samples/rpc/dubbo3/pom.xml b/samples/dubbo-samples/rpc/dubbo3/pom.xml
index fe9d5a4cc..3e9f73e07 100644
--- a/samples/dubbo-samples/rpc/dubbo3/pom.xml
+++ b/samples/dubbo-samples/rpc/dubbo3/pom.xml
@@ -21,7 +21,7 @@
 		UTF-8
 		UTF-8
 		2.7.16
-		2.2.5-SNAPSHOT
+		2.2.5
 		0.5.3
 		3.4.2
 		3.8.1
diff --git a/samples/sofaboot-samples/dynamic-stock/README.md b/samples/sofaboot-samples/dynamic-stock/README.md
index cbf418675..3729c76ab 100644
--- a/samples/sofaboot-samples/dynamic-stock/README.md
+++ b/samples/sofaboot-samples/dynamic-stock/README.md
@@ -42,7 +42,7 @@ biz 也是普通 SOFABoot,修改打包插件方式为 sofaArk biz 模块打包
 
     com.alipay.sofa
     sofa-ark-maven-plugin
-    2.2.5-SNAPSHOT
+    2.2.5
     
         
             default-cli
diff --git a/samples/sofaboot-samples/pom.xml b/samples/sofaboot-samples/pom.xml
index 9fc151590..ceac3d9f4 100644
--- a/samples/sofaboot-samples/pom.xml
+++ b/samples/sofaboot-samples/pom.xml
@@ -21,7 +21,7 @@
 
     
         1.8
-        2.2.5-SNAPSHOT
+        2.2.5
         0.5.5-SNAPSHOT
         2.9.1
         1.3.2
diff --git a/samples/springboot-samples/pom.xml b/samples/springboot-samples/pom.xml
index 84d5ba242..42f14a35e 100644
--- a/samples/springboot-samples/pom.xml
+++ b/samples/springboot-samples/pom.xml
@@ -20,7 +20,7 @@
     
         2.7.16
         1.8
-        2.2.6-SNAPSHOT
+        2.2.5
         0.5.5-SNAPSHOT
         3.4.2
         1.7.1
diff --git a/samples/springboot-samples/web/tomcat/biz1/src/main/java/com/alipay/sofa/web/biz1/Biz1Application.java b/samples/springboot-samples/web/tomcat/biz1/src/main/java/com/alipay/sofa/web/biz1/Biz1Application.java
index 019bee68d..a9cff4b9e 100644
--- a/samples/springboot-samples/web/tomcat/biz1/src/main/java/com/alipay/sofa/web/biz1/Biz1Application.java
+++ b/samples/springboot-samples/web/tomcat/biz1/src/main/java/com/alipay/sofa/web/biz1/Biz1Application.java
@@ -1,10 +1,7 @@
 package com.alipay.sofa.web.biz1;
 
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.WebApplicationType;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.builder.SpringApplicationBuilder;
-import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.core.io.DefaultResourceLoader;
 import org.springframework.core.io.ResourceLoader;
 
diff --git a/samples/springboot-samples/web/webflux/biz/pom.xml b/samples/springboot-samples/web/webflux/biz/pom.xml
index b411617ed..d8a346bc4 100644
--- a/samples/springboot-samples/web/webflux/biz/pom.xml
+++ b/samples/springboot-samples/web/webflux/biz/pom.xml
@@ -49,7 +49,7 @@
 			
 				com.alipay.sofa
 				sofa-ark-maven-plugin
-				2.2.5-SNAPSHOT
+				2.2.5
 				
 					
 						default-cli
diff --git a/samples/springboot3-samples/db/mybatis/README.md b/samples/springboot3-samples/db/mybatis/README.md
index 82e119767..43a2e127a 100644
--- a/samples/springboot3-samples/db/mybatis/README.md
+++ b/samples/springboot3-samples/db/mybatis/README.md
@@ -102,7 +102,7 @@ biz 包含两个模块,分别为 biz1 和 biz2, 都是普通 springboot,修
 
     com.alipay.sofa
     sofa-ark-maven-plugin
-    2.2.5-SNAPSHOT
+    2.2.5
     
         
             default-cli
diff --git a/samples/springboot3-samples/logging/README.md b/samples/springboot3-samples/logging/README.md
index dc57fd288..2ffa81042 100644
--- a/samples/springboot3-samples/logging/README.md
+++ b/samples/springboot3-samples/logging/README.md
@@ -67,7 +67,7 @@ biz 包含两个模块,分别为 biz1 和 biz2, 都是普通 springboot,修
 
     com.alipay.sofa
     sofa-ark-maven-plugin
-    2.2.5-SNAPSHOT
+    2.2.5
     
         
             default-cli
diff --git a/samples/springboot3-samples/msg/kafka/README.md b/samples/springboot3-samples/msg/kafka/README.md
index 2c94f0e20..9224d6b5e 100644
--- a/samples/springboot3-samples/msg/kafka/README.md
+++ b/samples/springboot3-samples/msg/kafka/README.md
@@ -78,7 +78,7 @@ biz 包含两个模块,分别为 biz1 和 biz2, 都是普通 springboot,修
 
     com.alipay.sofa
     sofa-ark-maven-plugin
-    2.2.5-SNAPSHOT
+    2.2.5
     
         
             default-cli
diff --git a/samples/springboot3-samples/web/tomcat/README.md b/samples/springboot3-samples/web/tomcat/README.md
index 75b003571..5ba6b6091 100644
--- a/samples/springboot3-samples/web/tomcat/README.md
+++ b/samples/springboot3-samples/web/tomcat/README.md
@@ -39,7 +39,7 @@ biz 包含两个模块,分别为 biz1 和 biz2, 都是普通 springboot,修
 
     com.alipay.sofa
     sofa-ark-maven-plugin
-    2.2.5-SNAPSHOT
+    2.2.5
     
         
             default-cli
diff --git a/sofa-serverless-runtime/pom.xml b/sofa-serverless-runtime/pom.xml
index 7deae8aef..bf2f39386 100644
--- a/sofa-serverless-runtime/pom.xml
+++ b/sofa-serverless-runtime/pom.xml
@@ -9,7 +9,7 @@
     pom
 
     
-        2.2.5-SNAPSHOT
+        2.2.5
         2.7.15
         1.2.9
         0.5.5-SNAPSHOT

From 5a8b3f282e69e5022ae56bf4fdee1ea25f8184b5 Mon Sep 17 00:00:00 2001
From: leojames 
Date: Fri, 22 Dec 2023 11:24:56 +0800
Subject: [PATCH 11/89] update to 0.5.5

---
 samples/sofaboot-samples/pom.xml   | 2 +-
 samples/springboot-samples/pom.xml | 2 +-
 sofa-serverless-runtime/pom.xml    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/samples/sofaboot-samples/pom.xml b/samples/sofaboot-samples/pom.xml
index ceac3d9f4..c2099534b 100644
--- a/samples/sofaboot-samples/pom.xml
+++ b/samples/sofaboot-samples/pom.xml
@@ -22,7 +22,7 @@
     
         1.8
         2.2.5
-        0.5.5-SNAPSHOT
+        0.5.5
         2.9.1
         1.3.2
         5.1.46
diff --git a/samples/springboot-samples/pom.xml b/samples/springboot-samples/pom.xml
index 42f14a35e..cfa236e7c 100644
--- a/samples/springboot-samples/pom.xml
+++ b/samples/springboot-samples/pom.xml
@@ -21,7 +21,7 @@
         2.7.16
         1.8
         2.2.5
-        0.5.5-SNAPSHOT
+        0.5.5
         3.4.2
         1.7.1
         0.6.1
diff --git a/sofa-serverless-runtime/pom.xml b/sofa-serverless-runtime/pom.xml
index bf2f39386..48444876c 100644
--- a/sofa-serverless-runtime/pom.xml
+++ b/sofa-serverless-runtime/pom.xml
@@ -12,7 +12,7 @@
         2.2.5
         2.7.15
         1.2.9
-        0.5.5-SNAPSHOT
+        0.5.5
         UTF-8
         UTF-8
         1.8

From a042f2199922945d8a30c8d137a06bb1239e6b3e Mon Sep 17 00:00:00 2001
From: leojames 
Date: Fri, 22 Dec 2023 12:27:20 +0800
Subject: [PATCH 12/89] update docs

---
 docs/content/zh-cn/docs/faq/faq.md            |   59 +
 .../module-development/module-dev-arkctl.md   |    2 +-
 docs/public/404.html                          |  182 +-
 docs/public/_print/home/index.html            |  782 ++-
 .../index.html"                               |  271 +-
 .../index.html"                               |  328 +-
 .../index.html"                               |  316 +-
 .../index.html"                               |  279 +-
 docs/public/blog/_print/index.html            |  437 +-
 docs/public/blog/index.html                   |  319 +-
 docs/public/blog/page/1/index.html            |   11 +-
 docs/public/categories/index.html             |  183 +-
 docs/public/categories/index.xml              |   19 +-
 docs/public/community/_print/index.html       |  421 +-
 docs/public/community/index.html              |  421 +-
 docs/public/docs/_print/index.html            | 5873 ++++++++++++++---
 .../contribution-guidelines/_print/index.html | 1991 +++++-
 .../arkctl/_print/index.html                  |  263 +-
 .../arkctl/architecture/index.html            |  492 +-
 .../contribution-guidelines/arkctl/index.html |  504 +-
 .../arklet/_print/index.html                  |  699 +-
 .../arklet/architecture/index.html            |  946 ++-
 .../arklet/how-to-release/index.html          |  528 +-
 .../contribution-guidelines/arklet/index.html |  512 +-
 .../communication-channel/index.html          |  535 +-
 .../contribution/_print/index.html            |  523 +-
 .../contribution/first-pr/index.html          |  601 +-
 .../index.html                                |  595 +-
 .../contribution/index.html                   |  528 +-
 .../contribution/local-dev-test/index.html    |  524 +-
 .../contribution/sermon/index.html            |  506 +-
 .../docs/contribution-guidelines/index.html   |  568 +-
 .../module-controller/_print/index.html       |  445 +-
 .../module-controller/architecture/index.html |  522 +-
 .../core-code-structure/index.html            |  496 +-
 .../crd-definition/index.html                 |  547 +-
 .../module-controller/index.html              |  536 +-
 .../module-lifecycle/index.html               |  516 +-
 .../sequence-diagram/index.html               |  512 +-
 .../our-vision/index.html                     |  573 +-
 .../role-and-promotion/index.html             |  583 +-
 .../runtime/_print/index.html                 |  499 +-
 .../runtime/ehcache/index.html                |  629 +-
 .../runtime/index.html                        |  520 +-
 .../runtime/logback/index.html                |  480 ++
 .../runtime/logj42/index.html                 |  584 +-
 .../sofa-ark/_print/index.html                |  246 +-
 .../sofa-ark/index.html                       |  504 +-
 docs/public/docs/faq/_print/index.html        |  331 +-
 docs/public/docs/faq/faq/index.html           |  548 ++
 .../index.html                                |  514 +-
 docs/public/docs/faq/index.html               |  512 +-
 docs/public/docs/index.html                   |  547 +-
 .../docs/introduction/_print/index.html       |  645 +-
 .../architecture/_print/index.html            |  472 +-
 .../architecture/arch-principle/index.html    |  578 +-
 .../class-delegation-principle/index.html     |  654 +-
 .../docs/introduction/architecture/index.html |  512 +-
 docs/public/docs/introduction/index.html      |  520 +-
 .../industry-background/index.html            |  565 +-
 .../intro-and-scenario/index.html             |  532 +-
 .../public/docs/quick-start/_print/index.html |  263 +-
 docs/public/docs/quick-start/index.html       |  531 +-
 docs/public/docs/tutorials/_print/index.html  | 3283 ++++++---
 .../tutorials/base-create/_print/index.html   |  308 +-
 .../docs/tutorials/base-create/index.html     |  504 +-
 .../springboot-and-sofaboot/index.html        |  550 +-
 docs/public/docs/tutorials/index.html         |  536 +-
 .../tutorials/module-create/_print/index.html |  428 +-
 .../docs/tutorials/module-create/index.html   |  512 +-
 .../module-create/init_by_archtype/index.html |  491 +-
 .../springboot-and-sofaboot/index.html        |  666 +-
 .../module-development/_print/index.html      | 1803 +++--
 .../coding-specification/index.html           |  521 +-
 .../tutorials/module-development/index.html   |  584 +-
 .../module-and-base-communication/index.html  |  781 ++-
 .../module-debug/index.html                   |  764 ++-
 .../module-dev-arkctl/index.html              |  601 +-
 .../module-slimming/index.html                |  692 +-
 .../reuse-base-datasource/index.html          |  652 +-
 .../reuse-base-interceptor/index.html         |  643 +-
 .../runtime-compatibility-list/index.html     |  696 +-
 .../runtime-service-route/index.html          |  480 ++
 .../module-development/sofa-ark/index.html    |  503 +-
 .../static-merge-deployment/index.html        |  577 +-
 .../module-operation/_print/index.html        |  901 ++-
 .../arklet-standalone-usage/index.html        |  493 +-
 .../crd-definition/index.html                 |  516 +-
 .../index.html                                |  515 +-
 .../tutorials/module-operation/index.html     |  568 +-
 .../module-deployment-and-rollback/index.html |  572 +-
 .../module-information-viewing/index.html     |  528 +-
 .../module-online-and-offline/index.html      |  571 +-
 .../module-scale-and-replace/index.html       |  571 +-
 .../module-service/index.html                 |  663 +-
 .../index.html                                |  558 +-
 .../tutorials/trial_step_by_step/index.html   |  860 ++-
 .../docs/video-training/_print/index.html     |  249 +-
 docs/public/docs/video-training/index.html    |  515 +-
 docs/public/home/index.html                   |  782 ++-
 docs/public/index.html                        |  187 +-
 docs/public/index.xml                         |  166 +-
 ...ef13b166dbdfa627fd0a495c66e11577c026aa3.js |    5 +
 ...1fd60d2e11a7d4b1020f3be5653eda0e5f8cc3a.js |    5 +
 ...2555c3f7cf1f6f0216c4527424189c537230618.js |    5 -
 docs/public/no/404.html                       |  167 +-
 docs/public/no/categories/index.html          |  168 +-
 docs/public/no/categories/index.xml           |   19 +-
 docs/public/no/index.html                     |  166 +-
 docs/public/no/index.xml                      |   18 +-
 docs/public/no/sitemap.xml                    |   42 +-
 docs/public/no/tags/index.html                |  168 +-
 docs/public/no/tags/index.xml                 |   19 +-
 .../search/fragment/zh-cn_10a5166.pf_fragment |  Bin 813 -> 0 bytes
 .../search/fragment/zh-cn_129e997.pf_fragment |  Bin 805 -> 0 bytes
 .../search/fragment/zh-cn_12c72db.pf_fragment |  Bin 557 -> 0 bytes
 .../search/fragment/zh-cn_1626e9a.pf_fragment |  Bin 2474 -> 0 bytes
 .../search/fragment/zh-cn_17b6c95.pf_fragment |  Bin 22608 -> 0 bytes
 .../search/fragment/zh-cn_1e91b9f.pf_fragment |  Bin 52570 -> 0 bytes
 .../search/fragment/zh-cn_2565708.pf_fragment |  Bin 711 -> 0 bytes
 .../search/fragment/zh-cn_2b50ca7.pf_fragment |  Bin 4703 -> 0 bytes
 .../search/fragment/zh-cn_2cdce58.pf_fragment |  Bin 1778 -> 0 bytes
 .../search/fragment/zh-cn_333cb5d.pf_fragment |  Bin 658 -> 0 bytes
 .../search/fragment/zh-cn_3375c59.pf_fragment |  Bin 3219 -> 0 bytes
 .../search/fragment/zh-cn_37abc26.pf_fragment |  Bin 597 -> 0 bytes
 .../search/fragment/zh-cn_3e505e2.pf_fragment |  Bin 841 -> 0 bytes
 .../search/fragment/zh-cn_3eebb18.pf_fragment |  Bin 3841 -> 0 bytes
 .../search/fragment/zh-cn_437ff34.pf_fragment |  Bin 5183 -> 0 bytes
 .../search/fragment/zh-cn_452ce63.pf_fragment |  Bin 1960 -> 0 bytes
 .../search/fragment/zh-cn_48eabf5.pf_fragment |  Bin 621 -> 0 bytes
 .../search/fragment/zh-cn_49920b0.pf_fragment |  Bin 644 -> 0 bytes
 .../search/fragment/zh-cn_527e5a5.pf_fragment |  Bin 4232 -> 0 bytes
 .../search/fragment/zh-cn_52f8205.pf_fragment |  Bin 694 -> 0 bytes
 .../search/fragment/zh-cn_5334973.pf_fragment |  Bin 1714 -> 0 bytes
 .../search/fragment/zh-cn_5531392.pf_fragment |  Bin 1599 -> 0 bytes
 .../search/fragment/zh-cn_5858dcf.pf_fragment |  Bin 1415 -> 0 bytes
 .../search/fragment/zh-cn_5aa965f.pf_fragment |  Bin 1801 -> 0 bytes
 .../search/fragment/zh-cn_5b363a9.pf_fragment |  Bin 911 -> 0 bytes
 .../search/fragment/zh-cn_621912d.pf_fragment |  Bin 1846 -> 0 bytes
 .../search/fragment/zh-cn_643d5a6.pf_fragment |  Bin 930 -> 0 bytes
 .../search/fragment/zh-cn_68421bd.pf_fragment |  Bin 981 -> 0 bytes
 .../search/fragment/zh-cn_6c7424a.pf_fragment |  Bin 3236 -> 0 bytes
 .../search/fragment/zh-cn_6f5fa08.pf_fragment |  Bin 3623 -> 0 bytes
 .../search/fragment/zh-cn_73db1ea.pf_fragment |  Bin 662 -> 0 bytes
 .../search/fragment/zh-cn_761aead.pf_fragment |  Bin 12527 -> 0 bytes
 .../search/fragment/zh-cn_79fa3b7.pf_fragment |  Bin 595 -> 0 bytes
 .../search/fragment/zh-cn_85334db.pf_fragment |  Bin 2276 -> 0 bytes
 .../search/fragment/zh-cn_85e5c23.pf_fragment |  Bin 583 -> 0 bytes
 .../search/fragment/zh-cn_87a6708.pf_fragment |  Bin 4560 -> 0 bytes
 .../search/fragment/zh-cn_8a1bae2.pf_fragment |  Bin 1292 -> 0 bytes
 .../search/fragment/zh-cn_8f5839d.pf_fragment |  Bin 2118 -> 0 bytes
 .../search/fragment/zh-cn_911abf9.pf_fragment |  Bin 818 -> 0 bytes
 .../search/fragment/zh-cn_926797f.pf_fragment |  Bin 618 -> 0 bytes
 .../search/fragment/zh-cn_97a797d.pf_fragment |  Bin 4112 -> 0 bytes
 .../search/fragment/zh-cn_9cb1588.pf_fragment |  Bin 950 -> 0 bytes
 .../search/fragment/zh-cn_a36709c.pf_fragment |  Bin 4100 -> 0 bytes
 .../search/fragment/zh-cn_a7f8684.pf_fragment |  Bin 2092 -> 0 bytes
 .../search/fragment/zh-cn_a99b2a9.pf_fragment |  Bin 3896 -> 0 bytes
 .../search/fragment/zh-cn_a9a4dde.pf_fragment |  Bin 1599 -> 0 bytes
 .../search/fragment/zh-cn_abb59bd.pf_fragment |  Bin 599 -> 0 bytes
 .../search/fragment/zh-cn_abc5a79.pf_fragment |  Bin 632 -> 0 bytes
 .../search/fragment/zh-cn_acacd83.pf_fragment |  Bin 685 -> 0 bytes
 .../search/fragment/zh-cn_adf5184.pf_fragment |  Bin 936 -> 0 bytes
 .../search/fragment/zh-cn_b179444.pf_fragment |  Bin 2187 -> 0 bytes
 .../search/fragment/zh-cn_ba91d81.pf_fragment |  Bin 1419 -> 0 bytes
 .../search/fragment/zh-cn_be9e15d.pf_fragment |  Bin 1271 -> 0 bytes
 .../search/fragment/zh-cn_c2b5e49.pf_fragment |  Bin 728 -> 0 bytes
 .../search/fragment/zh-cn_c372f97.pf_fragment |  Bin 2526 -> 0 bytes
 .../search/fragment/zh-cn_c41362b.pf_fragment |  Bin 595 -> 0 bytes
 .../search/fragment/zh-cn_c5f0f4e.pf_fragment |  Bin 1592 -> 0 bytes
 .../search/fragment/zh-cn_c91c9e8.pf_fragment |  Bin 1165 -> 0 bytes
 .../search/fragment/zh-cn_d3991c8.pf_fragment |  Bin 3128 -> 0 bytes
 .../search/fragment/zh-cn_d7cd39f.pf_fragment |  Bin 2610 -> 0 bytes
 .../search/fragment/zh-cn_d96a376.pf_fragment |  Bin 2160 -> 0 bytes
 .../search/fragment/zh-cn_daf53f5.pf_fragment |  Bin 1709 -> 0 bytes
 .../search/fragment/zh-cn_db72f55.pf_fragment |  Bin 1380 -> 0 bytes
 .../search/fragment/zh-cn_e0ee7b1.pf_fragment |  Bin 620 -> 0 bytes
 .../search/fragment/zh-cn_e53dd77.pf_fragment |  Bin 1797 -> 0 bytes
 .../search/fragment/zh-cn_e753241.pf_fragment |  Bin 1913 -> 0 bytes
 .../search/fragment/zh-cn_e760e5b.pf_fragment |  Bin 738 -> 0 bytes
 .../search/fragment/zh-cn_e98b5e4.pf_fragment |  Bin 8371 -> 0 bytes
 .../search/fragment/zh-cn_ea4d20e.pf_fragment |  Bin 602 -> 0 bytes
 .../search/fragment/zh-cn_ebcf38a.pf_fragment |  Bin 2197 -> 0 bytes
 .../search/fragment/zh-cn_ef22332.pf_fragment |  Bin 634 -> 0 bytes
 .../search/fragment/zh-cn_f16c903.pf_fragment |  Bin 640 -> 0 bytes
 .../search/fragment/zh-cn_fc88f53.pf_fragment |  Bin 580 -> 0 bytes
 docs/public/search/index.html                 |  183 +-
 .../search/index/zh-cn_25a7695.pf_index       |  Bin 36793 -> 0 bytes
 .../search/index/zh-cn_5a2a4fe.pf_index       |  Bin 2662 -> 0 bytes
 .../search/index/zh-cn_7d5e9fc.pf_index       |  Bin 40412 -> 0 bytes
 .../search/index/zh-cn_95d5dd9.pf_index       |  Bin 36134 -> 0 bytes
 .../search/index/zh-cn_bab6571.pf_index       |  Bin 38604 -> 0 bytes
 .../search/index/zh-cn_cc28e9c.pf_index       |  Bin 37540 -> 0 bytes
 .../search/index/zh-cn_dcdc9fa.pf_index       |  Bin 36103 -> 0 bytes
 .../search/index/zh-cn_f5531fa.pf_index       |  Bin 37026 -> 0 bytes
 docs/public/search/pagefind-entry.json        |    2 +-
 .../pagefind.zh-cn_ef5fdd1d36db6.pf_meta      |  Bin 988 -> 0 bytes
 docs/public/sitemap.xml                       |   17 +-
 docs/public/tags/index.html                   |  183 +-
 docs/public/tags/index.xml                    |   19 +-
 docs/public/user-cases/_print/index.html      |  605 +-
 .../public/user-cases/alibaba-aidc/index.html |  569 +-
 docs/public/user-cases/all-users/index.html   |  447 +-
 docs/public/user-cases/ant-group/index.html   |  375 +-
 docs/public/user-cases/index.html             |  354 +-
 docs/public/zh-cn/index.html                  |   11 +-
 docs/public/zh-cn/sitemap.xml                 |  276 +-
 207 files changed, 60853 insertions(+), 4894 deletions(-)
 create mode 100644 docs/content/zh-cn/docs/faq/faq.md
 create mode 100644 docs/public/docs/contribution-guidelines/runtime/logback/index.html
 create mode 100644 docs/public/docs/faq/faq/index.html
 create mode 100644 docs/public/docs/tutorials/module-development/runtime-service-route/index.html
 create mode 100644 docs/public/js/main.min.1eb4262674b2d02aa8d18559fef13b166dbdfa627fd0a495c66e11577c026aa3.js
 create mode 100644 docs/public/js/main.min.5ea1489ff282dac019c3662f41fd60d2e11a7d4b1020f3be5653eda0e5f8cc3a.js
 delete mode 100644 docs/public/js/main.min.6b611378dd7aa9db092fab7032555c3f7cf1f6f0216c4527424189c537230618.js
 delete mode 100644 docs/public/search/fragment/zh-cn_10a5166.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_129e997.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_12c72db.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_1626e9a.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_17b6c95.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_1e91b9f.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_2565708.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_2b50ca7.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_2cdce58.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_333cb5d.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_3375c59.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_37abc26.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_3e505e2.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_3eebb18.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_437ff34.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_452ce63.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_48eabf5.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_49920b0.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_527e5a5.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_52f8205.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_5334973.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_5531392.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_5858dcf.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_5aa965f.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_5b363a9.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_621912d.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_643d5a6.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_68421bd.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_6c7424a.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_6f5fa08.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_73db1ea.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_761aead.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_79fa3b7.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_85334db.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_85e5c23.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_87a6708.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_8a1bae2.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_8f5839d.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_911abf9.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_926797f.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_97a797d.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_9cb1588.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_a36709c.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_a7f8684.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_a99b2a9.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_a9a4dde.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_abb59bd.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_abc5a79.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_acacd83.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_adf5184.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_b179444.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_ba91d81.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_be9e15d.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_c2b5e49.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_c372f97.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_c41362b.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_c5f0f4e.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_c91c9e8.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_d3991c8.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_d7cd39f.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_d96a376.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_daf53f5.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_db72f55.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_e0ee7b1.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_e53dd77.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_e753241.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_e760e5b.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_e98b5e4.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_ea4d20e.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_ebcf38a.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_ef22332.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_f16c903.pf_fragment
 delete mode 100644 docs/public/search/fragment/zh-cn_fc88f53.pf_fragment
 delete mode 100644 docs/public/search/index/zh-cn_25a7695.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_5a2a4fe.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_7d5e9fc.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_95d5dd9.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_bab6571.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_cc28e9c.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_dcdc9fa.pf_index
 delete mode 100644 docs/public/search/index/zh-cn_f5531fa.pf_index
 delete mode 100644 docs/public/search/pagefind.zh-cn_ef5fdd1d36db6.pf_meta

diff --git a/docs/content/zh-cn/docs/faq/faq.md b/docs/content/zh-cn/docs/faq/faq.md
new file mode 100644
index 000000000..c8d88dae4
--- /dev/null
+++ b/docs/content/zh-cn/docs/faq/faq.md
@@ -0,0 +1,59 @@
+---
+title: 常见问题列表
+weight: 10
+---
+
+#### 问题 1-1:模块 compile 引入 springboot 依赖,模块安装时报错
+```text
+java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+```
+##### 解决方式
+模块需要做好瘦身,参考这里:[模块瘦身](/docs/tutorials/module-development/module-slimming.md)
+
+#### 问题 1-2:模块安装找不到 `ServerlessApplicationListener`
+报错信息如下:
+```text
+com.alipay.sofa.ark.exception.ArkLoaderException: [ArkBiz Loader] module1:1.0-SNAPSHOT : can not load class: com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+```
+##### 解决方式
+请在模块里面添加如下依赖:
+```xml
+
+    com.alipay.sofa.serverless
+    sofa-serverless-app-starter
+    0.5.5
+
+```
+或者升级 sofa-serverless 版本到最新版本
+
+#### 问题 1-3: 通过 go install 无法安装 arkctl
+执行如下命令,报错
+```shell
+go install serverless.alipay.com/sofa-serverless/v1/arkctl@latest
+```
+报错信息如下:
+```text
+go: serverless.alipay.com/sofa-serverless/v1/arkctl@latest: module serverless.alipay.com/sofa-serverless/v1/arkctl: Get "https://proxy.golang.org/serverless.alipay.com/sofa-serverless/v1/arkctl/@v/list": dial tcp 142.251.42.241:443: i/o timeout
+```
+##### 解决方式
+arkctl 是作为 sofa-serverless 子目录的方式存在的,所以没法直接 go get,可以从这下面下载执行文件, 请参考[安装 arkctl](https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0)
+
+#### 问题 1-4:模块安装报 `Master biz environment is null`
+解决方式,升级 sofa-serverless 版本到最新版本
+```xml
+
+    com.alipay.sofa.serverless
+    sofa-serverless-app-starter
+    ${最新版本号}
+
+```
+
+#### 问题 1-5:模块静态合并部署无法从制定的目录里找到模块包
+解决方式,升级 sofa-serverless 版本到最新版本
+```xml
+
+    com.alipay.sofa.serverless
+    sofa-serverless-app-starter
+    ${最新版本号}
+
+```
\ No newline at end of file
diff --git a/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md b/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md
index 20fd3de38..9f7f8e318 100644
--- a/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md
+++ b/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md
@@ -12,7 +12,7 @@ weight: 400
 
 方法二:
 
-1. 在 [二进制列表]([https://github.com/sofastack/sofa-serverless/tree/master/arkctl/bin](https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0)) 中下载对应的二进制并加入到本地
+1. 在 [二进制列表]([https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0](https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0)) 中下载对应的二进制并加入到本地
    path 中。
 
 ### 本地快速部署
diff --git a/docs/public/404.html b/docs/public/404.html
index 78ab36e12..da5b7230b 100644
--- a/docs/public/404.html
+++ b/docs/public/404.html
@@ -1,7 +1,177 @@
-404 Page not found | SOFAServerless
-
-
+
+
+  
+    
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+404 Page not found | SOFAServerless
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
 
-

Not found

Oops! This page doesn't exist. Try going back to the home page.

You can learn how to make a 404 page like this in Custom 404 Pages.

- - \ No newline at end of file + + + +
+ +
+
+
+
+

Not found

+

Oops! This page doesn't exist. Try going back to the home page.

+

You can learn how to make a 404 page like this in Custom 404 Pages.

+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/_print/home/index.html b/docs/public/_print/home/index.html index 455cbeb33..21273cdea 100644 --- a/docs/public/_print/home/index.html +++ b/docs/public/_print/home/index.html @@ -1,11 +1,773 @@ -SOFAServerless | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +SOFAServerless | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

让普通应用低成本享受 Serverless 体验,帮助企业降本增效!

- -

产品介绍

SOFAServerless 是一种模块化 Serverless 技术解决方案,它能让普通应用低成本演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护, -与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。

十亿级可统计的企业线上每分钟流量。50%企业需求交付效率提升。75%长尾应用机器数量减少。

适用场景

大幅加速应用构建和发布:传统应用镜像化构建+发布速度很慢,通过模块化方式,应用单次构建+发布耗时可从 5 分钟级减少到 1 分钟。

实现 SDK 无感升级:借助 SOFAServerless 将应用依赖尽可能下沉到基座 (类似业务 Sidecar),可以实现 SDK 的无打扰升级。

极致裁剪长尾应用资源成本:通过 SOFAServerless 将多个应用合并部署在一起,可以实现大量的长尾应用服务器裁撤。

大幅提升应用研发协作效率:通过 SOFAServerless 将应用快速划分成多个模块 (代码包),且多个模块间可以同时迭代互不影响,进而大幅提升研发效率。

简化中台业务资产沉淀:支持低成本将业务公共代码下沉到基座并在基座上长出各种轻薄的功能模块,从而让组织分工更加合理、需求交付更加高效。

降低微服务的演进成本:支持业务架构低成本地在单体应用、多模块、独立微服务应用之间来回切换,从而轻松让应用架构与业务发展保持及时同步。

大幅加速应用构建与发布传统应用镜像化构建 + 发布速度很慢,通过模块化方式,应用单次构建+发布耗时可从 5 分钟级减少到 1 分钟SDK 无感升级借助 SOFAServerless 将应用依赖尽可能下沉到基座 (类似业务 Sidecar),可以实现 SDK 的无打扰升级极致裁剪长尾应用资源成本通过 SOFAServerless 将多个应用合并部署在一起,可以实现大量的长尾应用服务器裁撤大幅提升应用研发协作效率通过 SOFAServerless 将应用快速划分成多个模块 (代码包),且多个模块间可以同时迭代互不影响,进而大幅提升研发效率简化中台业务资产沉淀支持低成本将业务公共代码下沉到基座并在基座上长出各种轻薄的功能模块,从而让组织分工更加合理、需求交付更加高效降低微服务演进成本支持业务架构低成本地在单体应用、多模块、独立微服务应用之间来回切换,从而轻松让应用架构与业务发展保持及时同步

SOFAServerless 优势

Speed as you need: 十秒级构建与启动,应用多个功能之间独立并行迭代无阻塞。

Pay as you need: 模块粒度小,占用资源少,调度密度与资源复用率高。模块和基座支持自动弹性伸缩,按需部署。

Deploy as you need: 灵活部署:模块可合并部署也可独立部署。变更影响面小:一次部署只涉及模块自身代码变更和对应的机器变更。

Evolution as you need: 提供配套工具,传统应用能一键改造成模块,大应用能低成本拆分成模块,模块能轻松演进成微服务或者回到单体应用。

Speed as you needPay as you needDeploy as you need灵活部署:模块可合并部署也可独立部署变更影响面小:一次部署只涉及模块自身代码变更和对应的机器变更模块粒度小,占用资源少,调度密度与资源复用率高。模块和基座支持自动弹性伸缩,按需部署十秒级构建与启动,应用多个功能之间独立并行迭代无阻塞Evolution as you need提供配套工具,传统应用能一键改造成模块,大应用能低成本拆分成模块,模块能轻松演进成微服务或者回到单体应用

欢迎参与开源社区

所有人都可以提交 Pull Request。 -欢迎参与 SOFAServerless 开源社区!

欢迎加入社区协作钉钉群

社区钉钉群号:24970018417

欢迎加入社区协作微信群

- - \ No newline at end of file + + + +
+ +
+
+
+ +
+
+
+
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+

让普通应用低成本享受 Serverless 体验,帮助企业降本增效!

+ + + + + + + + + + +

产品介绍

+

SOFAServerless 是一种模块化 Serverless 技术解决方案,它能让普通应用低成本演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护, +与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。

+

十亿级可统计的企业线上每分钟流量。50%企业需求交付效率提升。75%长尾应用机器数量减少。

+
+ +
+

适用场景

+

大幅加速应用构建和发布:传统应用镜像化构建+发布速度很慢,通过模块化方式,应用单次构建+发布耗时可从 5 分钟级减少到 1 分钟。

+

实现 SDK 无感升级:借助 SOFAServerless 将应用依赖尽可能下沉到基座 (类似业务 Sidecar),可以实现 SDK 的无打扰升级。

+

极致裁剪长尾应用资源成本:通过 SOFAServerless 将多个应用合并部署在一起,可以实现大量的长尾应用服务器裁撤。

+

大幅提升应用研发协作效率:通过 SOFAServerless 将应用快速划分成多个模块 (代码包),且多个模块间可以同时迭代互不影响,进而大幅提升研发效率。

+

简化平台/中台搭建和业务资产沉淀:支持低成本将业务公共代码下沉到基座并在基座上长出各种轻薄的功能模块,从而让组织分工更加合理、需求交付更加高效。

+

降低微服务的演进成本:支持业务架构低成本地在单体应用、多模块、独立微服务应用之间来回切换,从而轻松让应用架构与业务发展保持及时同步。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 大幅加速应用构建与发布 + + + 传统应用镜像化构建 + 发布速度很慢, + 通过模块化方式,应用单次构建+发布 + 耗时可从 5 分钟级减少到 1 分钟 + + + + + + + + + + + + + + + + + + SDK 无感升级 + + + 借助 SOFAServerless 将应用依赖尽可 + 能下沉到基座 (类似业务 Sidecar),可 + 以实现 SDK 的无打扰升级 + + + + + + + + + + + + + + + + + + + + + + + + 极致裁剪长尾应用资源成本 + + + 通过 SOFAServerless 将多个应用合并 + 部署在一起,可以实现大量的长尾应用 + 服务器裁撤 + + + + + + + + + + + + + + + + + + + 大幅提升应用研发协作效率 + + + 通过 SOFAServerless 将应用快速划分成 + 多个模块 (代码包),且多个模块间可以同 + 时迭代互不影响,进而大幅提升研发效率 + + + + + + + + + + + + + + + + + 简化平台/中台搭建和业务资产沉淀 + + + 支持低成本将业务公共代码下沉到基座并 + 在基座上长出各种轻薄的功能模块,从而 + 让组织分工更加合理、需求交付更加高效 + + + + + + + + + + + + + + + + + + 降低微服务演进成本 + + + 支持业务架构低成本地在单体应用、多模 + 块、独立微服务应用之间来回切换,从而 + 轻松让应用架构与业务发展保持及时同步 + + + + + +
+

SOFAServerless 优势

+

Speed as you need: 十秒级构建与启动,应用多个功能之间独立并行迭代无阻塞。

+

Pay as you need: 模块粒度小,占用资源少,调度密度与资源复用率高。模块和基座支持自动弹性伸缩,按需部署。

+

Deploy as you need: 灵活部署:模块可合并部署也可独立部署。变更影响面小:一次部署只涉及模块自身代码变更和对应的机器变更。

+

Evolution as you need: 提供配套工具,传统应用能一键改造成模块,大应用能低成本拆分成模块,模块能轻松演进成微服务或者回到单体应用。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Speed as you need + + + + + + + + + Pay as you need + + + + + + + Deploy as you need + + + 灵活部署:模块可合并部署也可独立部署 + 变更影响面小:一次部署只涉及模块自身 + 代码变更和对应的机器变更 + + + 模块粒度小,占用资源少,调度密度与 + 资源复用率高。模块和基座支持自动弹 + 性伸缩,按需部署 + + + 十秒级构建与启动,应用多个功能之间 + 独立并行迭代无阻塞 + + + + + + + + Evolution as you need + + + 提供配套工具,传统应用能一键改造成模 + 块,大应用能低成本拆分成模块,模块能 + 轻松演进成微服务或者回到单体应用 + + + + + + +
+
+
+
+

欢迎参与开源社区

+

所有人都可以提交 Pull Request。 +欢迎参与 SOFAServerless 开源社区!

+
+
+

欢迎加入社区协作钉钉群

+

+ +

+

社区钉钉群号:24970018417

+
+
+

欢迎加入社区协作微信群

+

+ +

+
+
+
+
+ + +
+
+
+
+ +
+ + +
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git "a/docs/public/blog/2023/09/22/\344\272\247\345\223\201\345\217\221\345\270\203\350\256\260\345\275\225/index.html" "b/docs/public/blog/2023/09/22/\344\272\247\345\223\201\345\217\221\345\270\203\350\256\260\345\275\225/index.html" index fb038b5cf..283426f14 100644 --- "a/docs/public/blog/2023/09/22/\344\272\247\345\223\201\345\217\221\345\270\203\350\256\260\345\275\225/index.html" +++ "b/docs/public/blog/2023/09/22/\344\272\247\345\223\201\345\217\221\345\270\203\350\256\260\345\275\225/index.html" @@ -1,11 +1,262 @@ -产品发布记录 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +产品发布记录 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ +
+

产品发布记录

+ + + +

请阅读 GitHub 上的产品发布记录

+
+ + + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git "a/docs/public/blog/2023/09/22/\345\271\262\350\264\247\346\226\207\347\253\240\344\270\216\350\247\206\351\242\221/index.html" "b/docs/public/blog/2023/09/22/\345\271\262\350\264\247\346\226\207\347\253\240\344\270\216\350\247\206\351\242\221/index.html" index 9f27f88f4..842a8fac3 100644 --- "a/docs/public/blog/2023/09/22/\345\271\262\350\264\247\346\226\207\347\253\240\344\270\216\350\247\206\351\242\221/index.html" +++ "b/docs/public/blog/2023/09/22/\345\271\262\350\264\247\346\226\207\347\253\240\344\270\216\350\247\206\351\242\221/index.html" @@ -1,4 +1,26 @@ -干货文章与视频 | SOFAServerless + + + + + + + + + + + + + + + + + + +干货文章与视频 | SOFAServerless + + + + + + + + + + + + + + - - +https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg"/> + + + + + + + + + + + + + + + + + + + -

干货文章与视频

技术会议

QCon 大会 - SOFAServerless 微服务新架构的探索与实践

举办时间:2023.09
会议 PPT:点击此处

SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍

举办时间:2022.07
会议视频:https://www.bilibili.com/video/BV1nU4y1B7u3
会议 PPT:点击此处

技术分享视频

开源人 - SOFAArk 类隔离框架底层原理

发布时间:2022.06
https://www.bilibili.com/video/BV1gS4y1i7Fg

技术分享文章

蚂蚁 SOFAServerless 微服务新架构的探索与实践

发布时间:2023.08
https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ

SOFAServerless 应用架构助力业务 10 倍效率提升,探索微服务隔离与共享的新平衡

发布时间:2023.07
https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg


- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ +
+

干货文章与视频

+ + + +

技术会议

+

KCD 开发者交流会 深圳站

+

举办时间:20231216
+会议 PPT: 点击此处下载

+

QCon 大会 - SOFAServerless 微服务新架构的探索与实践

+

举办时间:2023.09
+会议 PPT:点击此处

+

SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍

+

举办时间:2022.07
+会议视频:https://www.bilibili.com/video/BV1nU4y1B7u3
+会议 PPT:点击此处

+

技术分享视频

+

开源人 - SOFAArk 类隔离框架底层原理

+

发布时间:2022.06
+https://www.bilibili.com/video/BV1gS4y1i7Fg

+

技术分享文章

+

蚂蚁 SOFAServerless 微服务新架构的探索与实践

+

发布时间:2023.08
+https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ

+

SOFAServerless 应用架构助力业务 10 倍效率提升,探索微服务隔离与共享的新平衡

+

发布时间:2023.07
+https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg

+
+ + + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git "a/docs/public/blog/2023/09/22/\347\244\276\345\214\272\344\274\232\350\256\256\347\272\252\350\246\201/index.html" "b/docs/public/blog/2023/09/22/\347\244\276\345\214\272\344\274\232\350\256\256\347\272\252\350\246\201/index.html" index 52b031eae..ea1460b6e 100644 --- "a/docs/public/blog/2023/09/22/\347\244\276\345\214\272\344\274\232\350\256\256\347\272\252\350\246\201/index.html" +++ "b/docs/public/blog/2023/09/22/\347\244\276\345\214\272\344\274\232\350\256\256\347\272\252\350\246\201/index.html" @@ -1,31 +1,319 @@ -社区会议纪要 | SOFAServerless + + + + + + + + + + + + + + + + + + +社区会议纪要 | SOFAServerless + + + + + + + + + + + + + + - - +SOFAServerless 23.04.03 社区会议 会议纪要详见:https://github.com/sofastack/sofa-ark/issues/635 视频回放:https://www.bilibili.com/video/BV1f84y1K7qd"/> + + + + + + + + + + + + + + + + + + + -

社区会议纪要

SOFAServerless 23.09.19 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/100

SOFAServerless 23.09.04 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/44

SOFAServerless 23.08.21 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/38
视频回放:https://www.bilibili.com/video/BV19r4y1R761

SOFAServerless 23.08.07 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/13

SOFAServerless 23.07.03 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/1
视频回放:https://www.bilibili.com/video/BV1dh4y1f7KW

SOFAServerless 23.06.05 社区会议

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/661

SOFAServerless 23.05.08 社区会议

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/636
视频回放:https://www.bilibili.com/video/BV1Qs4y1D7Lv

SOFAServerless 23.04.03 社区会议

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/635
视频回放:https://www.bilibili.com/video/BV1f84y1K7qd


- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ +
+

社区会议纪要

+ + + +

SOFAServerless 23.09.19 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/100

+

SOFAServerless 23.09.04 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/44

+

SOFAServerless 23.08.21 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/38
+视频回放:https://www.bilibili.com/video/BV19r4y1R761

+

SOFAServerless 23.08.07 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/13

+

SOFAServerless 23.07.03 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/1
+视频回放:https://www.bilibili.com/video/BV1dh4y1f7KW

+

SOFAServerless 23.06.05 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/661

+

SOFAServerless 23.05.08 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/636
+视频回放:https://www.bilibili.com/video/BV1Qs4y1D7Lv

+

SOFAServerless 23.04.03 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/635
+视频回放:https://www.bilibili.com/video/BV1f84y1K7qd

+
+ + + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git "a/docs/public/blog/2023/09/22/\350\216\267\345\245\226\346\203\205\345\206\265/index.html" "b/docs/public/blog/2023/09/22/\350\216\267\345\245\226\346\203\205\345\206\265/index.html" index 1715ba289..c1083fbaa 100644 --- "a/docs/public/blog/2023/09/22/\350\216\267\345\245\226\346\203\205\345\206\265/index.html" +++ "b/docs/public/blog/2023/09/22/\350\216\267\345\245\226\346\203\205\345\206\265/index.html" @@ -1,11 +1,270 @@ -获奖情况 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +获奖情况 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ +
+

获奖情况

+ + + +

中国信通院云原生技术创新案例奖

+

image.png
image.png

+
+ + + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/blog/_print/index.html b/docs/public/blog/_print/index.html index a2eb9ae18..0c96feb8e 100644 --- a/docs/public/blog/_print/index.html +++ b/docs/public/blog/_print/index.html @@ -1,8 +1,431 @@ -最新信息 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +最新信息 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

干货文章与视频

技术会议

QCon 大会 - SOFAServerless 微服务新架构的探索与实践

举办时间:2023.09
会议 PPT:点击此处

SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍

举办时间:2022.07
会议视频:https://www.bilibili.com/video/BV1nU4y1B7u3
会议 PPT:点击此处

技术分享视频

开源人 - SOFAArk 类隔离框架底层原理

发布时间:2022.06
https://www.bilibili.com/video/BV1gS4y1i7Fg

技术分享文章

蚂蚁 SOFAServerless 微服务新架构的探索与实践

发布时间:2023.08
https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ

SOFAServerless 应用架构助力业务 10 倍效率提升,探索微服务隔离与共享的新平衡

发布时间:2023.07
https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg


获奖情况

中国信通院云原生技术创新案例奖

image.png
image.png


社区会议纪要

SOFAServerless 23.09.19 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/100

SOFAServerless 23.09.04 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/44

SOFAServerless 23.08.21 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/38
视频回放:https://www.bilibili.com/video/BV19r4y1R761

SOFAServerless 23.08.07 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/13

SOFAServerless 23.07.03 社区会议

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/1
视频回放:https://www.bilibili.com/video/BV1dh4y1f7KW

SOFAServerless 23.06.05 社区会议

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/661

SOFAServerless 23.05.08 社区会议

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/636
视频回放:https://www.bilibili.com/video/BV1Qs4y1D7Lv

SOFAServerless 23.04.03 社区会议

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/635
视频回放:https://www.bilibili.com/video/BV1f84y1K7qd


产品发布记录

请阅读 GitHub 上的产品发布记录


- - \ No newline at end of file + + + +
+ +
+
+
+
+
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

最新信息

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + +
+

干货文章与视频

+ + +

技术会议

+

KCD 开发者交流会 深圳站

+

举办时间:20231216
+会议 PPT: 点击此处下载

+

QCon 大会 - SOFAServerless 微服务新架构的探索与实践

+

举办时间:2023.09
+会议 PPT:点击此处

+

SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍

+

举办时间:2022.07
+会议视频:https://www.bilibili.com/video/BV1nU4y1B7u3
+会议 PPT:点击此处

+

技术分享视频

+

开源人 - SOFAArk 类隔离框架底层原理

+

发布时间:2022.06
+https://www.bilibili.com/video/BV1gS4y1i7Fg

+

技术分享文章

+

蚂蚁 SOFAServerless 微服务新架构的探索与实践

+

发布时间:2023.08
+https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ

+

SOFAServerless 应用架构助力业务 10 倍效率提升,探索微服务隔离与共享的新平衡

+

发布时间:2023.07
+https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg

+
+ +
+ + + + + + + + + + + + + + + + +
+

获奖情况

+ + +

中国信通院云原生技术创新案例奖

+

image.png
image.png

+
+ +
+ + + + + + + + + + + + + + + + +
+

社区会议纪要

+ + +

SOFAServerless 23.09.19 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/100

+

SOFAServerless 23.09.04 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/44

+

SOFAServerless 23.08.21 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/38
+视频回放:https://www.bilibili.com/video/BV19r4y1R761

+

SOFAServerless 23.08.07 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/13

+

SOFAServerless 23.07.03 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/1
+视频回放:https://www.bilibili.com/video/BV1dh4y1f7KW

+

SOFAServerless 23.06.05 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/661

+

SOFAServerless 23.05.08 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/636
+视频回放:https://www.bilibili.com/video/BV1Qs4y1D7Lv

+

SOFAServerless 23.04.03 社区会议

+

会议纪要详见:https://github.com/sofastack/sofa-ark/issues/635
+视频回放:https://www.bilibili.com/video/BV1f84y1K7qd

+
+ +
+ + + + + + + + + + + + + + + + +
+

产品发布记录

+ + +

请阅读 GitHub 上的产品发布记录

+
+ +
+ + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/blog/index.html b/docs/public/blog/index.html index 7e123ce95..2a274deea 100644 --- a/docs/public/blog/index.html +++ b/docs/public/blog/index.html @@ -1,16 +1,313 @@ -最新信息 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +最新信息 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
张贴在 2023
  • 干货文章与视频

    Friday, September 22, 2023 在 最新信息

    技术会议 QCon 大会 - SOFAServerless 微服务新架构的探索与实践 举办时间:2023.09 + + + +

    + +
    +
    +
    +
    + + +
    + +
    +
    张贴在 2023
    +
      +
    • +
      +
      干货文章与视频
      +

      Friday, September 22, 2023 在 最新信息

      + + + + + + +

      技术会议 KCD 开发者交流会 深圳站 举办时间:20231216 会议 PPT: 点击此处下载 +QCon 大会 - SOFAServerless 微服务新架构的探索与实践 举办时间:2023.09 会议 PPT:点击此处 SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍 举办时间:2022.07 会议视频:https://www.bilibili.com/video/BV1nU4y1B7u3 会议 PPT:点击此处 -技术分享视频 开源人 - SOFAArk 类隔离框架底层原理 发布时间:2022.06 …

      更多

    • 获奖情况

      Friday, September 22, 2023 在 最新信息

      中国信通院云原生技术创新案例奖

      更多

    • 社区会议纪要

      Friday, September 22, 2023 在 最新信息

      SOFAServerless 23.09.19 社区会议 会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/100 SOFAServerless 23.09.04 社区会议 会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/44 SOFAServerless 23.08.21 社区会议 会议纪要详 …

      更多

    • 产品发布记录

      Friday, September 22, 2023 在 最新信息

      请阅读 GitHub 上的产品发布记录。

      更多

    - - \ No newline at end of file +技术分享视频 开源人 - …

    +

    更多

    +
    +
  • +
  • +
    +
    获奖情况
    +

    Friday, September 22, 2023 在 最新信息

    + + + + + + +

    中国信通院云原生技术创新案例奖

    +

    更多

    +
    +
  • +
  • +
    +
    社区会议纪要
    +

    Friday, September 22, 2023 在 最新信息

    + + + + + + +

    SOFAServerless 23.09.19 社区会议 会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/100 SOFAServerless 23.09.04 社区会议 会议纪要详见:https://github.com/sofastack/sofa-serverless/issues/44 SOFAServerless 23.08.21 社区会议 会议纪要详 …

    +

    更多

    +
    +
  • +
  • +
    +
    产品发布记录
    +

    Friday, September 22, 2023 在 最新信息

    + + + + + + +

    请阅读 GitHub 上的产品发布记录。 +

    +

    更多

    +
    +
  • +
+ +
+
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/blog/page/1/index.html b/docs/public/blog/page/1/index.html index 770891062..cf6099549 100644 --- a/docs/public/blog/page/1/index.html +++ b/docs/public/blog/page/1/index.html @@ -1 +1,10 @@ -/blog/ \ No newline at end of file + + + + /blog/ + + + + + + diff --git a/docs/public/categories/index.html b/docs/public/categories/index.html index 02d5e7e89..9b3cda738 100644 --- a/docs/public/categories/index.html +++ b/docs/public/categories/index.html @@ -1,7 +1,178 @@ -Categories | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Categories | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Categories

- - \ No newline at end of file + + + +
+ +
+
+
+
+
+

Categories

+
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/categories/index.xml b/docs/public/categories/index.xml index 1d0246fac..6c11702dd 100644 --- a/docs/public/categories/index.xml +++ b/docs/public/categories/index.xml @@ -1 +1,18 @@ -SOFAServerless – Categories/categories/Recent content in Categories on SOFAServerlessHugo -- gohugo.iozh-cn \ No newline at end of file + + + SOFAServerless – Categories + /categories/ + Recent content in Categories on SOFAServerless + Hugo -- gohugo.io + zh-cn + + + + + + + + + + + diff --git a/docs/public/community/_print/index.html b/docs/public/community/_print/index.html index eb7824975..6eb1e68b1 100644 --- a/docs/public/community/_print/index.html +++ b/docs/public/community/_print/index.html @@ -1,10 +1,413 @@ -参与社区 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +参与社区 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

加入 SOFAServerless 社区

SOFAServerless 是一个开源项目,社区中的任何人都可以使用、改善和尽情使用它。我们很期待你能加入我们!下面是如何查看最近更新以及参与我们的一些方式。

获取更多信息社区交流群,和我们一起讨论 Serverless!加入学习和沟通正在或打算使用 SOFAServerless ?加入我们完成您的第一次提交,想了解如何为SOFAServerless 做出贡献,请参考我们的贡献指南点击此处开发和贡献如果你想通过为 SOFAServerless 贡献更多
- - \ No newline at end of file + + + +
+ +
+
+
+
+
+
+
+ +

加入 SOFAServerless 社区

+

SOFAServerless 是一个开源项目,社区中的任何人都可以使用、改善和尽情使用它。我们很期待你能加入我们!下面是如何查看最近更新以及参与我们的一些方式。

+
+
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 获取更多信息 + + + + 社区交流群 + ,和我们一起讨论 Serverless! + + + 加入 + + + + + + + + + + + + + + + + + + + + + + + + + + + 学习和沟通 + + + 正在或打算使用 SOFAServerless ? + + + + + + + + + + + + + 加入我们 + + + + 完成您的第一次提交,想了解如何为SOFAServerless 做 + 出贡献,请参考我们的 + 贡献指南 + + + + + 点击此处 + + + + + + + + + + + + + + + + + + + + + + + + + 开发和贡献 + + + 如果你想通过为 SOFAServerless 贡献更多 + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/community/index.html b/docs/public/community/index.html index a95af1638..7e4a7aa63 100644 --- a/docs/public/community/index.html +++ b/docs/public/community/index.html @@ -1,10 +1,413 @@ -参与社区 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +参与社区 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

加入 SOFAServerless 社区

SOFAServerless 是一个开源项目,社区中的任何人都可以使用、改善和尽情使用它。我们很期待你能加入我们!下面是如何查看最近更新以及参与我们的一些方式。

获取更多信息社区交流群,和我们一起讨论 Serverless!加入学习和沟通正在或打算使用 SOFAServerless ?加入我们完成您的第一次提交,想了解如何为SOFAServerless 做出贡献,请参考我们的贡献指南点击此处开发和贡献如果你想通过为 SOFAServerless 贡献更多
- - \ No newline at end of file + + + +
+ +
+
+
+
+
+
+
+ +

加入 SOFAServerless 社区

+

SOFAServerless 是一个开源项目,社区中的任何人都可以使用、改善和尽情使用它。我们很期待你能加入我们!下面是如何查看最近更新以及参与我们的一些方式。

+
+
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 获取更多信息 + + + + 社区交流群 + ,和我们一起讨论 Serverless! + + + 加入 + + + + + + + + + + + + + + + + + + + + + + + + + + + 学习和沟通 + + + 正在或打算使用 SOFAServerless ? + + + + + + + + + + + + + 加入我们 + + + + 完成您的第一次提交,想了解如何为SOFAServerless 做 + 出贡献,请参考我们的 + 贡献指南 + + + + + 点击此处 + + + + + + + + + + + + + + + + + + + + + + + + + 开发和贡献 + + + 如果你想通过为 SOFAServerless 贡献更多 + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/_print/index.html b/docs/public/docs/_print/index.html index 414926dd7..6ddfd7311 100644 --- a/docs/public/docs/_print/index.html +++ b/docs/public/docs/_print/index.html @@ -1,936 +1,4949 @@ -产品文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +产品文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

这是本节的多页打印视图。 -点击此处打印.

返回本页常规视图.

产品文档

1 - 产品介绍

1.1 - 简介与适用场景

简介

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

适用场景

痛点 1:应用构建发布慢或者 SDK 升级繁琐

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

痛点 2:长尾应用资源成本高

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本

这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

痛点 3:企业研发协作效率低

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。

痛点 4:难以沉淀业务资产提高中台效率

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。

痛点 5:微服务演进成本高

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进



1.2 - 行业背景

微服务的问题

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

基础设施复杂

认知负载高

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

运维负担重

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

  1. 重要功能版本发布
  2. 修复紧急 bug
  3. 遇到重大安全漏洞

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

对于业务研发人员

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

对于基础设施人员

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

启动慢

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

发布效率低

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

协作与资源成本高

单体应用/大应用过大

image.png

多人协作阻塞

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

变更影响面大,风险高

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

小应用过多

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

资源成本高

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

长期维护成本

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

问题必然性

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

如何合理拆分微服务

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

微服务的拆分与业务和组织发展敏捷度不一致

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

不同体量企业面对的问题

image.png

行业尝试的解法

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. 只解决其中部分问题
  3. 存量应用接入改造成本高

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。



1.3 - 架构介绍

1.3.1 - 架构原理

模块化应用架构

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

模块的定义

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

  1. SpringBoot 打包生成的 jar 包
  2. 一个模块: 一个 SpringContext + 一个 ClassLoader
  3. 热部署(升级的时候不需要启动进程)

模块的隔离与共享

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

  1. Ark Container 提供多 ClassLoader 运行环境
  2. Arklet 来管理模块生命周期
  3. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  4. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  5. SpringContext Manager 提供 Bean 与服务发现调用机制
  6. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader

image.png

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

隔离方面

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

共享方面

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

模块间通信

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

模块的可演进

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

  1. 模块不占资源所以拆分没有资源成本
  2. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  3. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  4. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

模块化架构的优势

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

平台架构

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

image.png
组件视图

在整个平台里,需要四个组件:

  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  3. 控制面组件 ModuleController
    1. ModuleDeployment 提供模块发布与运维能力
    2. ModuleScheduler 提供模块调度能力
    3. ModuleScaler 提供模块伸缩能力

1.3.2 - 基座与模块间类委托加载原理介绍

多模块间类委托加载

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

当前类委托加载机制

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. 模块可以扫描到基座里的所有类:
    • 优势:模块可以引入较少依赖
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
  3. 模块不能扫描到基座里的任何资源:
    • 优势:不会与基座重复初始化相同的 Bean
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
  4. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。

使用时需要注意事项

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

类委托的最佳实践

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

强制委托加载

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

使用方法

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

优点

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

缺点

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

自定义委托加载

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. 基座里预置一些公共依赖(可选)。
  3. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座:
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
            <plugin>
-                <groupId>com.alipay.sofa</groupId>
-                <artifactId>sofa-ark-maven-plugin</artifactId>
-                <configuration> 
-                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
-                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
-                    <declaredMode>true</declaredMode>
-                </configuration>
-            </plugin>
-

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

  1. 只有模块声明过的依赖才可以委托给基座加载。

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

优点

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

缺点

对模块瘦身的依赖较强。

对比与总结

依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低

结论:推荐自定义委托加载方式

  1. 模块自定义委托加载 + 模块瘦身。
  2. 模块开启 declaredMode。
  3. 基座预置依赖。

declaredMode 开启方式

开启条件

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

# 如果是 SOFABoot,则:
-# 配置健康检查跳过 JVM 服务检查
-com.alipay.sofa.boot.skip-jvm-reference-health-check=true
-# 忽略未解析的占位符
-com.alipay.sofa.ignore.unresolvable.placeholders=true
-

开启方式

模块打包插件里增加如下配置:
image.png

开启后的副作用

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。


2 - 快速开始

实验 1:一键实现多应用合并部署

合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

  1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
  2. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署
    您也可以直接使用 官方 Demo 和文档 在本地完成实验。

小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。



实验 2:一键体验应用秒级热部署

步骤 1:本地软件安装

下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

步骤 2:一键启动 SOFAServerless

使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

步骤 3:秒级发布模块

执行以下命令:

kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml
-

即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
image.png

步骤 4:清理本地环境

您可以使用 make undev 删除所有本地资源,清理本地环境。



欢迎大家学习 SOFAServerless 视频教程

点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

3 - 视频教程

SOFAServerless 模块本地开发与上线视频教程

小贴士: 仅需两分钟时间



该视频的详细文字版教程请点击此处查看。

SOFAServerless 平台和研发框架完整视频教程

步骤 1:点击此处注册开源学堂账号。

步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

4 - 用户手册

4.1 - 基座接入

4.1.1 - SpringBoot 或 SOFABoot 升级为基座

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

代码与配置修改

修改 application.properties

# 需要定义应用名
-spring.application.name = ${替换为实际基座应用名}
-

修改主 pom.xml

<properties>
-    <sofa.ark.verion>2.2.5</sofa.ark.verion>
-    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
-</properties>
-
<dependency>
-    <groupId>com.alipay.sofa.serverless</groupId>
-    <artifactId>sofa-serverless-base-starter</artifactId>
-    <version>${sofa.serverless.runtime.version}</version>
-</dependency>
-
-<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
-<dependency>
-    <groupId>com.alipay.sofa</groupId>
-    <artifactId>web-ark-plugin</artifactId>
-</dependency>
-

启动验证

基座应用能正常启动即表示验证成功!



4.2 - 模块接入

4.2.1 - SpringBoot 或 SOFABoot 一键升级为模块

本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

前提条件

  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

接入步骤

步骤 1:修改 application.properties

# 需要定义应用名
-spring.application.name = ${替换为实际模块应用名}
-

步骤 2:添加模块需要的依赖和打包插件

特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;


-<plugins>
-    <!--这里添加ark 打包插件-->
-    <plugin>
-        <groupId>com.alipay.sofa</groupId>
-        <artifactId>sofa-ark-maven-plugin</artifactId>
-        <version>{sofa.ark.version}</version>
-        <executions>
-            <execution>
-                <id>default-cli</id>
-                <goals>
-                    <goal>repackage</goal>
-                </goals>
-            </execution>
-        </executions>
-        <configuration>
-            <skipArkExecutable>true</skipArkExecutable>
-            <outputDirectory>./target</outputDirectory>
-            <bizName>${替换为模块名}</bizName>
-            <webContextPath>${模块自定义的 web context path}</webContextPath>
-            <declaredMode>true</declaredMode>
-        </configuration>
-    </plugin>
-    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
-    <plugin>
-        <!--原来 spring-boot 打包插件 -->
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-maven-plugin</artifactId>
-    </plugin>
-</plugins>
-

步骤 3:自动化瘦身模块

您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 -扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

步骤 4:构建成模块 jar 包

执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

小贴士模块中支持的完整中间件清单

实验:验证模块既能独立启动,也能被合并部署

增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 -当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

验证能合并部署到基座上

  1. 启动上一步(验证能独立启动步骤)的基座
  2. 发起模块部署
curl --location --request POST 'localhost:1238/installBiz' \
---header 'Content-Type: application/json' \
---data '{
-    "bizName": "${模块名}",
-    "bizVersion": "${模块版本}",
-    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
-}'
-

返回如下信息表示模块安装成功
image.png

  1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider

image.png

  1. 卸载模块
curl --location --request POST 'localhost:1238/uninstallBiz' \
---header 'Content-Type: application/json' \
---data '{
-    "bizName": "dynamic-provider",
-    "bizVersion": "0.0.1-SNAPSHOT"
-}'
-

返回如下,表示卸载成功

{
-    "code": "SUCCESS",
-    "data": {
-        "code": "SUCCESS",
-        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
-    }
-}
-

验证能独立启动

普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
image.png

4.2.2 - 使用 maven archtype 脚手架自动生成

正在更新中,预计 11 月上线。

4.3 - 基座与模块并行开发验证

欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 -首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

  1. 合并部署
  2. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)

本文实验工程代码在:开源仓库 samples 目录库里

场景一:合并部署

先介绍第一个场景多应用合并部署,整体流程如下: -image.png

可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

1. 基座接入改造

  1. 为 **application.properties **增加应用名(如果没有的话):

spring.application.name=${基座应用名}

  1. 在 **pom.xml **里增加必要的依赖
<properties>
-    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
-</properties>
-<dependencies>
-    <dependency>
-        <groupId>com.alipay.sofa.serverless</groupId>
-        <artifactId>sofa-serverless-base-starter</artifactId>
-        <version>${sofa.serverless.runtime.version}</version>
-    </dependency>
-</dependencies>
-

理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

    <dependency>
-        <groupId>com.alipay.sofa</groupId>
-        <artifactId>web-ark-plugin</artifactId>
-    </dependency>
-
  1. 点击编译器启动基座。

2. 模块 1 接入改造

  1. 添加模块需要的依赖和打包插件
<plugins>
-    <!--这里添加ark 打包插件-->
-    <plugin>
-        <groupId>com.alipay.sofa</groupId>
-        <artifactId>sofa-ark-maven-plugin</artifactId>
-        <executions>
-            <execution>
-                <id>default-cli</id>
-                <goals>
-                    <goal>repackage</goal>
-                </goals>
-            </execution>
-        </executions>
-        <configuration>
-            <skipArkExecutable>true</skipArkExecutable>
-            <outputDirectory>./target</outputDirectory>
-            <bizName>${替换为模块名}</bizName>
-            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
-            <declaredMode>true</declaredMode>
-            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
-            <packExcludesConfig>rules.txt</packExcludesConfig>
-        </configuration>
-    </plugin>
-    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
-    <plugin>
-        <!--原来 spring-boot 打包插件 -->
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-maven-plugin</artifactId>
-    </plugin>
-</plugins>
-
  1. 参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

  2. 开发模块,例如增加 Rest Controller,提供 Rest 接口

@RestController
-public class SampleController {
-    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
-
-    @Autowired
-    private ApplicationContext applicationContext;
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-        String appName = applicationContext.getApplicationName();
-        LOGGER.info("{} web test: into sample controller", appName);
-        return String.format("hello to %s deploy", appName);
-    }
-}
-
  1. 点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

  2. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

hello to ${模块1名} deploy
-

3. 模块 1 开发与验证

开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

  1. 修改 Rest 代码
@RestController
-public class SampleController {
-    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
-
-    @Autowired
-    private ApplicationContext applicationContext;
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-        String appName = applicationContext.getApplicationName();
-        LOGGER.info("{} web test v2: into sample controller", appName);
-        return String.format("hello to %s deploy v2", appName);
-    }
-}
-
  1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
hello to ${模块1名} deploy v2
-

4. 模块 2 接入改造、开发与验证

模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

场景二:中台应用

中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: -image.png

可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI。 -接下来我们将介绍与合并部署不同的(即新增的)操作细节。

1. 基座完成通信类和 SPI 的定义

在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 -通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

  1. 新建 bundle,定义接口类
public class ProductInfo {
-    private String  name;
-    private String  author;
-    private String  src;
-    private Integer orderCount;
-}
-
  1. 定义 SPI
public interface StrategyService {
-    List<ProductInfo> strategy(List<ProductInfo> products);
-    String getAppName();
-}
-

2. 模块 1 引入通信类基座并实现基座 SPI

在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

  1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
  2. 定义 SPI 实现
@Service
-public class StrategyServiceImpl implements StrategyService {
-
-    @Autowired
-    private ApplicationContext applicationContext;
-
-    @Override
-    public List<ProductInfo> strategy(List<ProductInfo> products) {
-        return products;
-    }
-
-    @Override
-    public String getAppName() {
-        return applicationContext.getApplicationName();
-    }
-}
-
  1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回

biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

默认的 products 列表
-

3. 模块 2 引入通信类基座并实现基座 SPI

与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

@Service
-public class StrategyServiceImpl implements StrategyService {
-    @Autowired
-    private ApplicationContext applicationContext;
-
-    @Override
-    public List<ProductInfo> strategy(List<ProductInfo> products) {
-        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
-        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
-        return products;
-    }
-
-    @Override
-    public String getAppName() {
-        return applicationContext.getApplicationName();
-    }
-}
-
更改排序后的 products 列表
-

基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

文档中的链接地址

  1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
  2. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
  3. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
  4. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
  5. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/

4.4 - 模块研发

4.4.1 - 编码规范

基础规范

  1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
  2. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
  3. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布

知识点

模块瘦身 (重要)
模块与模块、模块与基座通信 (重要)
模块测试 (重要)
模块复用基座拦截器
模块复用基座数据源
基座与模块间类委托加载原理介绍



4.4.2 - 模块瘦身

为什么要瘦身

为了让模块安装更快、内存消耗更小:

  • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
  • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。

一键自动瘦身

瘦身原则

构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

步骤一

在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

excludeGroupIds=org.apache*
-excludeArtifactIds=commons-lang
-

步骤二

在模块打包插件中,引入上述配置文件:

    <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
-    <plugin>
-        <groupId>com.alipay.sofa</groupId>
-        <artifactId>sofa-ark-maven-plugin</artifactId>
-        <version>2.2.5-SNAPSHOT</version>
-        <executions>
-            <execution>
-                <id>default-cli</id>
-                <goals>
-                    <goal>repackage</goal>
-                </goals>
-            </execution>
-        </executions>
-        <configuration>
-            <skipArkExecutable>true</skipArkExecutable>
-            <outputDirectory>./target</outputDirectory>
-            <bizName>biz1</bizName>
-            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
-            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
-            <packExcludesConfig>rules.txt</packExcludesConfig>
-            <webContextPath>biz1</webContextPath>
-            <declaredMode>true</declaredMode>
-            <!--					打包、安装和发布 ark biz-->
-            <!--					静态合并部署需要配置-->
-            <!--					<attach>true</attach>-->
-        </configuration>
-    </plugin>
-

步骤三

打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

基本原理

SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

  1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
  2. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。

基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

手动排包瘦身

模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
image.png

如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
image.png
确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
image.png

在 pom 中统一排包(更彻底)

有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

<dependency>
-    <groupId>com.serverless.mymodule</groupId>
-    <artifactId>mymodule-core</artifactId>
-    <exclusions>
-          <exclusion>
-              <groupId>org.springframework</groupId>
-              <artifactId>*</artifactId>
-          </exclusion>
-          <exclusion>
-              <groupId>org.apache.commons</groupId>
-              <artifactId>*</artifactId>
-          </exclusion>
-          <exclusion>
-              <groupId>......</groupId>
-              <artifactId>*</artifactId>
-          </exclusion>
-    </exclusions>
-</dependency>
-

在 sofa-ark-maven-plugin 中指定排包

通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

 <plugin>
-      <groupId>com.alipay.sofa</groupId>
-      <artifactId>sofa-ark-maven-plugin</artifactId>
-      <executions>
-          <execution>
-              <id>default-cli</id>
-              <goals>
-                  <goal>repackage</goal>
-              </goals>
-          </execution>
-      </executions>
-      <configuration>
-          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
-          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
-          <outputDirectory>../../target</outputDirectory>
-          <bizName>mymodule</bizName>
-          <finalName>mymodule-${project.version}-${timestamp}</finalName>
-          <bizVersion>${project.version}-${timestamp}</bizVersion>
-          <webContextPath>/mymodule</webContextPath>
-      </configuration>
-  </plugin>
-


4.4.3 - 模块与模块、模块与基座通信

基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 -当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

Spring 环境

基座调用模块

只能使用 SpringServiceFinder

@RestController
-public class SampleController {
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-
-        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
-                "studentProvider", Provider.class);
-        Result result = studentProvider.provide(new Param());
-
-        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
-                "teacherProvider", Provider.class);
-        Result result1 = teacherProvider.provide(new Param());
-        
-        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
-                Provider.class);
-        for (String beanName : providerMap.keySet()) {
-            Result result2 = providerMap.get(beanName).provide(new Param());
-        }
-
-        return "hello to ark master biz";
-    }
-}
-

模块调用基座

方式一:注解 @AutowiredFromBase

@RestController
-public class SampleController {
-
-    @AutowiredFromBase(name = "sampleServiceImplNew")
-    private SampleService sampleServiceImplNew;
-
-    @AutowiredFromBase(name = "sampleServiceImpl")
-    private SampleService sampleServiceImpl;
-
-    @AutowiredFromBase
-    private List<SampleService> sampleServiceList;
-
-    @AutowiredFromBase
-    private Map<String, SampleService> sampleServiceMap;
-
-    @AutowiredFromBase
-    private AppService appService;
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-
-        sampleServiceImplNew.service();
-
-        sampleServiceImpl.service();
-
-        for (SampleService sampleService : sampleServiceList) {
-            sampleService.service();
-        }
-
-        for (String beanName : sampleServiceMap.keySet()) {
-            sampleServiceMap.get(beanName).service();
-        }
-
-        appService.getAppName();
-
-        return "hello to ark2 dynamic deploy";
-    }
-}
-

方式二:编程API SpringServiceFinder

@RestController
-public class SampleController {
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-
-        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
-        String result = sampleServiceImplFromFinder.service();
-        System.out.println(result);
-
-        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
-        for (String beanName : sampleServiceMapFromFinder.keySet()) {
-            String result1 = sampleServiceMapFromFinder.get(beanName).service();
-            System.out.println(result1);
-        }
-
-        return "hello to ark2 dynamic deploy";
-    }
-}
-

模块调用模块

参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

方式一:注解 @AutowiredFromBiz

@RestController
-public class SampleController {
-
-    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
-    private Provider studentProvider;
-
-    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
-    private Provider teacherProvider;
-
-    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
-    private List<Provider> providers;
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-
-        Result provide = studentProvider.provide(new Param());
-
-        Result provide1 = teacherProvider.provide(new Param());
-
-        for (Provider provider : providers) {
-            Result provide2 = provider.provide(new Param());
-        }
-
-        return "hello to ark2 dynamic deploy";
-    }
-}
-

方式二:编程API SpringServiceFinder

@RestController
-public class SampleController {
-
-    @RequestMapping(value = "/", method = RequestMethod.GET)
-    public String hello() {
-
-        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
-        Result result1 = teacherProvider1.provide(new Param());
-
-        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
-        for (String beanName : providerMap.keySet()) {
-            Result result2 = providerMap.get(beanName).provide(new Param());
-        }
-
-        return "hello to ark2 dynamic deploy";
-    }
-}
-

完整样例

SOFABoot 环境

请参考该文档



4.4.4 - 模式本地开发

Arkctl 工具安装

方法一:

  1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
  2. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。

方法二:

  1. 二进制列表 中下载对应的二进制并加入到本地 -path 中。

本地快速部署

你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

准备:

  1. 在本地启动一个基座。
  2. 打开一个模块项目仓库。

执行命令:

# 需要在仓库的根目录下执行。
-# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
-arkctl deploy
-

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

准备:

  1. 在本地启动一个基座。
  2. 准备一个构建好的 jar 包。

执行命令:

arkctl deploy /path/to/your/pre/built/bundle-biz.jar
-

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

准备:

  1. 在远程已经运行起来的基座 pod。
  2. 打开一个模块项目仓库。
  3. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。

执行命令:

# 需要在仓库的根目录下执行。
-# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
-arkctl deploy --pod {namespace}/{podName}
-

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

准备:

  1. 在本地启动一个基座。
  2. 打开一个多模块 Maven 项目仓库。

执行命令:

# 需要在仓库的根目录下执行。
-# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
-arkctl deploy --sub ./path/to/your/sub/module
-

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

场景 5: 查询当前基座中已经部署的模块。

准备:

  1. 在本地启动一个基座。

执行命令:

arkctl status
-

场景 6: 查询远程 k8s 环境基座中已经部署的模块。

准备:

  1. 在远程 k8s 环境启动一个基座。
  2. 确保本地有 kube 证书以及有关权限。

执行命令:

arkctl status --pod {namespace}/{name}
-

4.4.5 - 模式测试

本地调试

您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
Arklet HTTP 接口主要提供了以下能力:

  1. 部署和卸载模块。
  2. 查询所有已部署的模块信息。
  3. 查询各项系统和业务指标。

部署模块

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
-

请求体样例:

{
-    "bizName": "test",
-    "bizVersion": "1.0.0",
-    // local path should start with file://, alse support remote url which can be downloaded
-    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
-}
-

部署成功返回结果样例:

{
-  "code":"SUCCESS",
-  "data":{
-    "bizInfos":[
-      {
-        "bizName":"dynamic-provider",
-        "bizState":"ACTIVATED",
-        "bizVersion":"1.0.0",
-        "declaredMode":true,
-        "identity":"dynamic-provider:1.0.0",
-        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
-        "priority":100,
-        "webContextPath":"provider"
-      }
-    ],
-    "code":"SUCCESS",
-    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
-  }
-}
-

部署失败返回结果样例:

{
-  "code":"FAILED",
-  "data":{
-    "code":"REPEAT_BIZ",
-    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
-  }
-}
-

卸载模块

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
-

请求体样例:

{
-    "bizName":"dynamic-provider",
-    "bizVersion":"1.0.0"
-}
-

卸载成功返回结果样例:

{
-  "code":"SUCCESS"
-}
-

卸载失败返回结果样例:

{
-  "code":"FAILED",
-  "data":{
-    "code":"NOT_FOUND_BIZ",
-    "message":"Uninstall biz: test:1.0.0 not found."
-  }
-}
-

查询模块

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
-

请求体样例:

{}
-

返回结果样例:

{
-  "code":"SUCCESS",
-  "data":[
-    {
-      "bizName":"dynamic-provider",
-      "bizState":"ACTIVATED",
-      "bizVersion":"1.0.0",
-      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
-      "webContextPath":"provider"
-    },
-    {
-      "bizName":"stock-mng",
-      "bizState":"ACTIVATED",
-      "bizVersion":"1.0.0",
-      "mainClass":"embed main",
-      "webContextPath":"/"
-    }
-  ]
-}
-

获取帮助

Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
-

请求体样例:

{}
-

返回结果样例:

{
-    "code":"SUCCESS",
-    "data":[
-        {
-            "desc":"query all ark biz(including master biz)",
-            "id":"queryAllBiz"
-        },
-        {
-            "desc":"list all supported commands",
-            "id":"help"
-        },
-        {
-            "desc":"uninstall one ark biz",
-            "id":"uninstallBiz"
-        },
-        {
-            "desc":"switch one ark biz",
-            "id":"switchBiz"
-        },
-        {
-            "desc":"install one ark biz",
-            "id":"installBiz"
-        }
-    ]
-}
-

本地构建如何不改变模块版本号

添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

<profile>
-    <id>local</id>
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>com.alipay.sofa</groupId>
-                <artifactId>sofa-ark-maven-plugin</artifactId>
-                <configuration>
-                    <finalName>${project.artifactId}-${project.version}</finalName>
-                    <bizVersion>${project.version}</bizVersion>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
-</profile>
-

单元测试

模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。



4.4.6 - 复用基座拦截器

诉求

基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

解法

为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

步骤 1:

基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

public interface AnnotionService {
-    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
-}
-

步骤 2:

在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

@Service
-@SofaService(uniqueId = "facadeAroundHandler")
-public class FacadeAroundHandler implements AnnotionService {
-
-    private final static Logger LOG = LoggerConst.MY_LOGGER;
-
-    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
-        log.info("开始执行")
-        joinPoint.proceed();
-        log.info("执行完成")
-    }
-}
-

步骤 3:

在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

//注意,这里不必申明成一个bean,不要加@Component或者@Service
-@Aspect
-public class FacadeAroundAspect {
-
-    @SofaReference(uniqueId = "facadeAroundHandler")
-    private AnnotionService facadeAroundHandler;
-
-    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
-    public void facadeAroundPointcut() {
-    }
-
-    @Around("facadeAroundPointcut()")
-    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
-        return facadeAroundHandler.doAround(joinPoint);
-    }
-}
-

步骤 4:

使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

@Configuration
-public class MngAspectConfiguration {
-    @Bean
-    public FacadeAroundAspect facadeAroundAspect() {
-        return new FacadeAroundAspect();
-    }
-    @Bean
-    public EnvRouteAspect envRouteAspect() {
-        return new EnvRouteAspect();
-    }
-    @Bean
-    public FacadeAroundAspect facadeAroundAspect() {
-        return new FacadeAroundAspect();
-    }
-    
-}
-

步骤 5:

模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

@SpringBootApplication
-@ImportResource("classpath*:META-INF/spring/*.xml")
-@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
-public class ModuleBootstrapApplication {
-    public static void main(String[] args) {
-        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
-        	.web(WebApplicationType.NONE);
-        builder.build().run(args);
-    }
-}
-


4.4.7 - 复用基座数据源

建议

强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

SpringBoot 解法

在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
通过BaseAppUtils.getBean获取到基座的 Bean 对象,然后注册成模块的 Bean:


-@Configuration
-@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
-@EnableTransactionManagement
-public class MybatisConfig {
-
-    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
-    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
-
-    @Bean(name = "transactionManager")
-    public PlatformTransactionManager platformTransactionManager() {
-        return (PlatformTransactionManager) BaseAppUtils.getBean("transactionManager");
-    }
-
-    @Bean(name = "transactionTemplate")
-    public TransactionTemplate transactionTemplate() {
-        return (TransactionTemplate) BaseAppUtils.getBean("transactionTemplate");
-    }
-
-    @Bean(name = "mysqlSqlFactory")
-    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
-        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
-        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBean("dataSource");
-        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
-        mysqlSqlFactory.setDataSource(dataSource);
-        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
-                .getResources("classpath:mapper/*.xml"));
-        return mysqlSqlFactory;
-    }
-}
-

SOFABoot 解法

如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
image.png


-@Configuration
-@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
-@EnableTransactionManagement
-public class MybatisConfig {
-
-    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
-    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
-    
-    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
-
-    @Bean(name = "transactionManager")
-    public PlatformTransactionManager platformTransactionManager() {
-        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
-    }
-
-    @Bean(name = "transactionTemplate")
-    public TransactionTemplate transactionTemplate() {
-        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
-    }
-
-    @Bean(name = "mysqlSqlFactory")
-    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
-        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
-        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
-        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
-        mysqlSqlFactory.setDataSource(dataSource);
-        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
-                .getResources("classpath:mapper/*.xml"));
-        return mysqlSqlFactory;
-    }
-}
-


4.4.8 - 静态合并部署

介绍

SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 + + + +

+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

产品文档

+ + + + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - 产品介绍

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

1.1 - 简介与适用场景

+ +

简介

+

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

+

适用场景

+

痛点 1:应用构建发布慢或者 SDK 升级繁琐

+

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

+ +

痛点 2:长尾应用资源成本高

+

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本
+
+这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

+

痛点 3:企业研发协作效率低

+

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。
+

+

痛点 4:难以沉淀业务资产提高中台效率

+

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。
+

+

痛点 5:微服务演进成本高

+

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

+

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

+

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进
+

+
+
+ +
+ + + + + + + + + + + +
+ +

1.2 - 行业背景

+ +

微服务的问题

+

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

+

基础设施复杂

+

认知负载高

+

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

+

运维负担重

+

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

+
    +
  1. 重要功能版本发布
  2. +
  3. 修复紧急 bug
  4. +
  5. 遇到重大安全漏洞
  6. +
+

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

+
对于业务研发人员
+

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

+
对于基础设施人员
+

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

+

启动慢

+

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

+

发布效率低

+

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

+

协作与资源成本高

+

单体应用/大应用过大

+

image.png

+
多人协作阻塞
+

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

+
变更影响面大,风险高
+

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

+

小应用过多

+

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

+
资源成本高
+

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

+
长期维护成本
+

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

+

问题必然性

+

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

+

如何合理拆分微服务

+

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

+

微服务的拆分与业务和组织发展敏捷度不一致

+

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

+

不同体量企业面对的问题

+

image.png

+

行业尝试的解法

+

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

+
    +
  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. +
  3. 只解决其中部分问题
  4. +
  5. 存量应用接入改造成本高
  6. +
+

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。

+
+
+ +
+ + + + + + + + + + + +
+ +

1.3 - 架构介绍

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

1.3.1 - 架构原理

+ +

模块化应用架构

+

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

+

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

+

模块的定义

+

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

+
    +
  1. SpringBoot 打包生成的 jar 包
  2. +
  3. 一个模块: 一个 SpringContext + 一个 ClassLoader
  4. +
  5. 热部署(升级的时候不需要启动进程)
  6. +
+

模块的隔离与共享

+

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

+
    +
  1. Ark Container 提供多 ClassLoader 运行环境
  2. +
  3. Arklet 来管理模块生命周期
  4. +
  5. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  6. +
  7. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  8. +
  9. SpringContext Manager 提供 Bean 与服务发现调用机制
  10. +
  11. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader
  12. +
+

image.png

+

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

+

隔离方面

+

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

+

共享方面

+

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

+

模块间通信

+

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

+

模块的可演进

+

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

+
    +
  1. 模块不占资源所以拆分没有资源成本
  2. +
  3. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  4. +
  5. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  6. +
  7. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式
  8. +
+

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

+

模块化架构的优势

+

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

+

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

+

平台架构

+

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

+

image.png
组件视图

+

在整个平台里,需要四个组件:

+
    +
  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. +
  3. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  4. +
  5. 控制面组件 ModuleController +
      +
    1. ModuleDeployment 提供模块发布与运维能力
    2. +
    3. ModuleScheduler 提供模块调度能力
    4. +
    5. ModuleScaler 提供模块伸缩能力
    6. +
    +
  6. +
+
+ +
+ + + + + + + + + + + +
+ +

1.3.2 - 基座与模块间类委托加载原理介绍

+ +

多模块间类委托加载

+

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

+

当前类委托加载机制

+

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

+
    +
  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. +
  3. 模块可以扫描到基座里的所有类: +
      +
    • 优势:模块可以引入较少依赖
    • +
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
    • +
    +
  4. +
  5. 模块不能扫描到基座里的任何资源: +
      +
    • 优势:不会与基座重复初始化相同的 Bean
    • +
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
    • +
    +
  6. +
  7. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。
  8. +
+

使用时需要注意事项

+

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

+

类委托的最佳实践

+

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

+

强制委托加载

+

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

+

使用方法

+

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

+

优点

+

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

+

缺点

+

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

+

自定义委托加载

+

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

+
    +
  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. +
  3. 基座里预置一些公共依赖(可选)。
  4. +
  5. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座: +
      +
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. +
    3. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
    4. +
    +
  6. +
+
            <plugin>
+                <groupId>com.alipay.sofa</groupId>
+                <artifactId>sofa-ark-maven-plugin</artifactId>
+                <configuration> 
+                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
+                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
+                    <declaredMode>true</declaredMode>
+                </configuration>
+            </plugin>
+

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

+
    +
  1. 只有模块声明过的依赖才可以委托给基座加载。
  2. +
+

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

+

优点

+

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

+

缺点

+

对模块瘦身的依赖较强。

+

对比与总结

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低
+

结论:推荐自定义委托加载方式

+
    +
  1. 模块自定义委托加载 + 模块瘦身。
  2. +
  3. 模块开启 declaredMode。
  4. +
  5. 基座预置依赖。
  6. +
+

declaredMode 开启方式

+

开启条件

+

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

+
# 如果是 SOFABoot,则:
+# 配置健康检查跳过 JVM 服务检查
+com.alipay.sofa.boot.skip-jvm-reference-health-check=true
+# 忽略未解析的占位符
+com.alipay.sofa.ignore.unresolvable.placeholders=true
+

开启方式

+

模块打包插件里增加如下配置:
image.png

+

开启后的副作用

+

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。

+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +

2 - 快速开始

+ +

实验 1:一键实现多应用合并部署

+

合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

+
    +
  1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
  2. +
  3. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署。 +
    +您也可以直接使用 官方 Demo 和文档 在本地完成实验。
  4. +
+

小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。

+
+
+

实验 2:一键体验应用秒级热部署

+

步骤 1:本地软件安装

+

下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

+

步骤 2:一键启动 SOFAServerless

+

使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

+

步骤 3:秒级发布模块

+

执行以下命令:

+
kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml
+

即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
image.png

+

步骤 4:清理本地环境

+

您可以使用 make undev 删除所有本地资源,清理本地环境。

+
+
+

欢迎大家学习 SOFAServerless 视频教程

+

点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +

3 - 视频教程

+ +

SOFAServerless 模块本地开发与上线视频教程

+

小贴士: 仅需两分钟时间

+ +
+
+

该视频的详细文字版教程请点击此处查看。

+

SOFAServerless 平台和研发框架完整视频教程

+

步骤 1:点击此处注册开源学堂账号。

+

步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +

4 - 用户手册

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

4.1 - 基座接入

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

4.1.1 - SpringBoot 或 SOFABoot 升级为基座

+ +

前提条件

+
    +
  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. +
  3. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
  4. +
+

接入步骤

+

代码与配置修改

+

修改 application.properties

+
# 需要定义应用名
+spring.application.name = ${替换为实际基座应用名}
+

修改主 pom.xml

+
<properties>
+    <sofa.ark.verion>2.2.5</sofa.ark.verion>
+    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
+</properties>
+
<dependency>
+    <groupId>com.alipay.sofa.serverless</groupId>
+    <artifactId>sofa-serverless-base-starter</artifactId>
+    <version>${sofa.serverless.runtime.version}</version>
+</dependency>
+
+<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
+<dependency>
+    <groupId>com.alipay.sofa</groupId>
+    <artifactId>web-ark-plugin</artifactId>
+</dependency>
+

启动验证

+

基座应用能正常启动即表示验证成功!

+
+
+ +
+ + + + + + + + + + + + + + + +
+ +

4.2 - 模块接入

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

4.2.1 - SpringBoot 或 SOFABoot 一键升级为模块

+ +

本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

+

前提条件

+
    +
  1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
  2. +
  3. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
  4. +
+

接入步骤

+

步骤 1:修改 application.properties

+
# 需要定义应用名
+spring.application.name = ${替换为实际模块应用名}
+

步骤 2:添加模块需要的依赖和打包插件

+

特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

+

+<plugins>
+    <!--这里添加ark 打包插件-->
+    <plugin>
+        <groupId>com.alipay.sofa</groupId>
+        <artifactId>sofa-ark-maven-plugin</artifactId>
+        <version>{sofa.ark.version}</version>
+        <executions>
+            <execution>
+                <id>default-cli</id>
+                <goals>
+                    <goal>repackage</goal>
+                </goals>
+            </execution>
+        </executions>
+        <configuration>
+            <skipArkExecutable>true</skipArkExecutable>
+            <outputDirectory>./target</outputDirectory>
+            <bizName>${替换为模块名}</bizName>
+            <webContextPath>${模块自定义的 web context path}</webContextPath>
+            <declaredMode>true</declaredMode>
+        </configuration>
+    </plugin>
+    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
+    <plugin>
+        <!--原来 spring-boot 打包插件 -->
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-maven-plugin</artifactId>
+    </plugin>
+</plugins>
+

步骤 3:自动化瘦身模块

+

您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 +扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

+

步骤 4:构建成模块 jar 包

+

执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

+

小贴士模块中支持的完整中间件清单

+

实验:验证模块既能独立启动,也能被合并部署

+

增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 +当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

+

验证能合并部署到基座上

+
    +
  1. 启动上一步(验证能独立启动步骤)的基座
  2. +
  3. 发起模块部署
  4. +
+
curl --location --request POST 'localhost:1238/installBiz' \
+--header 'Content-Type: application/json' \
+--data '{
+    "bizName": "${模块名}",
+    "bizVersion": "${模块版本}",
+    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
+}'
+

返回如下信息表示模块安装成功
image.png

+
    +
  1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider
  2. +
+

image.png

+
    +
  1. 卸载模块
  2. +
+
curl --location --request POST 'localhost:1238/uninstallBiz' \
+--header 'Content-Type: application/json' \
+--data '{
+    "bizName": "dynamic-provider",
+    "bizVersion": "0.0.1-SNAPSHOT"
+}'
+

返回如下,表示卸载成功

+
{
+    "code": "SUCCESS",
+    "data": {
+        "code": "SUCCESS",
+        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
+    }
+}
+

验证能独立启动

+

普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
image.png

+ +
+ + + + + + + + + + + +
+ +

4.2.2 - 使用 maven archtype 脚手架自动生成

+ +

正在更新中,预计 11 月上线。

+ +
+ + + + + + + + + + + + + + + +
+ +

4.3 - 基座与模块并行开发验证

+ +

欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 +首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

+
    +
  1. 合并部署
  2. +
  3. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)
  4. +
+
+

本文实验工程代码在:开源仓库 samples 目录库里

+
+

场景一:合并部署

+

先介绍第一个场景多应用合并部署,整体流程如下: +image.png

+

可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

+

1. 基座接入改造

+
    +
  1. 为 **application.properties **增加应用名(如果没有的话):
  2. +
+

spring.application.name=${基座应用名}

+
    +
  1. 在 **pom.xml **里增加必要的依赖
  2. +
+
<properties>
+    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
+</properties>
+<dependencies>
+    <dependency>
+        <groupId>com.alipay.sofa.serverless</groupId>
+        <artifactId>sofa-serverless-base-starter</artifactId>
+        <version>${sofa.serverless.runtime.version}</version>
+    </dependency>
+</dependencies>
+

+

理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

+
    <dependency>
+        <groupId>com.alipay.sofa</groupId>
+        <artifactId>web-ark-plugin</artifactId>
+    </dependency>
+
    +
  1. 点击编译器启动基座。
  2. +
+

2. 模块 1 接入改造

+
    +
  1. 添加模块需要的依赖和打包插件
  2. +
+
<plugins>
+    <!--这里添加ark 打包插件-->
+    <plugin>
+        <groupId>com.alipay.sofa</groupId>
+        <artifactId>sofa-ark-maven-plugin</artifactId>
+        <executions>
+            <execution>
+                <id>default-cli</id>
+                <goals>
+                    <goal>repackage</goal>
+                </goals>
+            </execution>
+        </executions>
+        <configuration>
+            <skipArkExecutable>true</skipArkExecutable>
+            <outputDirectory>./target</outputDirectory>
+            <bizName>${替换为模块名}</bizName>
+            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
+            <declaredMode>true</declaredMode>
+            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
+            <packExcludesConfig>rules.txt</packExcludesConfig>
+        </configuration>
+    </plugin>
+    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
+    <plugin>
+        <!--原来 spring-boot 打包插件 -->
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-maven-plugin</artifactId>
+    </plugin>
+</plugins>
+
    +
  1. +

    参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

    +
  2. +
  3. +

    开发模块,例如增加 Rest Controller,提供 Rest 接口

    +
  4. +
+
@RestController
+public class SampleController {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+        String appName = applicationContext.getApplicationName();
+        LOGGER.info("{} web test: into sample controller", appName);
+        return String.format("hello to %s deploy", appName);
+    }
+}
+
    +
  1. +

    点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

    +
  2. +
  3. +

    执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

    +
  4. +
+
hello to ${模块1名} deploy
+

3. 模块 1 开发与验证

+

开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

+
    +
  1. 修改 Rest 代码
  2. +
+
@RestController
+public class SampleController {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+        String appName = applicationContext.getApplicationName();
+        LOGGER.info("{} web test v2: into sample controller", appName);
+        return String.format("hello to %s deploy v2", appName);
+    }
+}
+
    +
  1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
  2. +
+
hello to ${模块1名} deploy v2
+

4. 模块 2 接入改造、开发与验证

+

模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

+

场景二:中台应用

+

中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: +image.png

+

可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI。 +接下来我们将介绍与合并部署不同的(即新增的)操作细节。

+

1. 基座完成通信类和 SPI 的定义

+

在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 +通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

+
    +
  1. 新建 bundle,定义接口类
  2. +
+
public class ProductInfo {
+    private String  name;
+    private String  author;
+    private String  src;
+    private Integer orderCount;
+}
+
    +
  1. 定义 SPI
  2. +
+
public interface StrategyService {
+    List<ProductInfo> strategy(List<ProductInfo> products);
+    String getAppName();
+}
+

2. 模块 1 引入通信类基座并实现基座 SPI

+

在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

+
    +
  1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
  2. +
  3. 定义 SPI 实现
  4. +
+
@Service
+public class StrategyServiceImpl implements StrategyService {
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @Override
+    public List<ProductInfo> strategy(List<ProductInfo> products) {
+        return products;
+    }
+
+    @Override
+    public String getAppName() {
+        return applicationContext.getApplicationName();
+    }
+}
+
    +
  1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回
  2. +
+

biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

+
默认的 products 列表
+

3. 模块 2 引入通信类基座并实现基座 SPI

+

与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

+
@Service
+public class StrategyServiceImpl implements StrategyService {
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @Override
+    public List<ProductInfo> strategy(List<ProductInfo> products) {
+        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
+        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
+        return products;
+    }
+
+    @Override
+    public String getAppName() {
+        return applicationContext.getApplicationName();
+    }
+}
+
更改排序后的 products 列表
+

基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

+

文档中的链接地址

+
    +
  1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
  2. +
  3. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
  4. +
  5. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
  6. +
  7. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
  8. +
  9. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/
  10. +
+ +
+ + + + + + + + + + + +
+ +

4.4 - 模块研发

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

4.4.1 - 编码规范

+ +

基础规范

+
    +
  1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
  2. +
  3. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
  4. +
  5. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布
  6. +
+

知识点

+

模块瘦身 (重要)
+模块与模块、模块与基座通信 (重要)
+模块测试 (重要)
+模块复用基座拦截器
+模块复用基座数据源
+基座与模块间类委托加载原理介绍

+
+
+ +
+ + + + + + + + + + + +
+ +

4.4.2 - 模块瘦身

+ +

为什么要瘦身

+

为了让模块安装更快、内存消耗更小:

+
    +
  • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
  • +
  • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。
  • +
+

一键自动瘦身

+

瘦身原则

+

构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

+

步骤一

+

在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

+
excludeGroupIds=org.apache*
+excludeArtifactIds=commons-lang
+

步骤二

+

在模块打包插件中,引入上述配置文件:

+
    <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
+    <plugin>
+        <groupId>com.alipay.sofa</groupId>
+        <artifactId>sofa-ark-maven-plugin</artifactId>
+        <version>2.2.5</version>
+        <executions>
+            <execution>
+                <id>default-cli</id>
+                <goals>
+                    <goal>repackage</goal>
+                </goals>
+            </execution>
+        </executions>
+        <configuration>
+            <skipArkExecutable>true</skipArkExecutable>
+            <outputDirectory>./target</outputDirectory>
+            <bizName>biz1</bizName>
+            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
+            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
+            <packExcludesConfig>rules.txt</packExcludesConfig>
+            <webContextPath>biz1</webContextPath>
+            <declaredMode>true</declaredMode>
+            <!--					打包、安装和发布 ark biz-->
+            <!--					静态合并部署需要配置-->
+            <!--					<attach>true</attach>-->
+        </configuration>
+    </plugin>
+

步骤三

+

打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

+

您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

+

基本原理

+

SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

+
    +
  1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
  2. +
  3. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。
  4. +
+

+

基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

+

手动排包瘦身

+

模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
image.png

+

如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
image.png
确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
image.png

+

在 pom 中统一排包(更彻底)

+

有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

+
<dependency>
+    <groupId>com.serverless.mymodule</groupId>
+    <artifactId>mymodule-core</artifactId>
+    <exclusions>
+          <exclusion>
+              <groupId>org.springframework</groupId>
+              <artifactId>*</artifactId>
+          </exclusion>
+          <exclusion>
+              <groupId>org.apache.commons</groupId>
+              <artifactId>*</artifactId>
+          </exclusion>
+          <exclusion>
+              <groupId>......</groupId>
+              <artifactId>*</artifactId>
+          </exclusion>
+    </exclusions>
+</dependency>
+

在 sofa-ark-maven-plugin 中指定排包

+

通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

+
 <plugin>
+      <groupId>com.alipay.sofa</groupId>
+      <artifactId>sofa-ark-maven-plugin</artifactId>
+      <executions>
+          <execution>
+              <id>default-cli</id>
+              <goals>
+                  <goal>repackage</goal>
+              </goals>
+          </execution>
+      </executions>
+      <configuration>
+          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
+          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
+          <outputDirectory>../../target</outputDirectory>
+          <bizName>mymodule</bizName>
+          <finalName>mymodule-${project.version}-${timestamp}</finalName>
+          <bizVersion>${project.version}-${timestamp}</bizVersion>
+          <webContextPath>/mymodule</webContextPath>
+      </configuration>
+  </plugin>
+

+
+ +
+ + + + + + + + + + + +
+ +

4.4.3 - 模块与模块、模块与基座通信

+ +

基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 +当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

+

Spring 环境

+

基座调用模块

+

只能使用 SpringServiceFinder

+
@RestController
+public class SampleController {
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+
+        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
+                "studentProvider", Provider.class);
+        Result result = studentProvider.provide(new Param());
+
+        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
+                "teacherProvider", Provider.class);
+        Result result1 = teacherProvider.provide(new Param());
+        
+        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
+                Provider.class);
+        for (String beanName : providerMap.keySet()) {
+            Result result2 = providerMap.get(beanName).provide(new Param());
+        }
+
+        return "hello to ark master biz";
+    }
+}
+

模块调用基座

+

方式一:注解 @AutowiredFromBase

+
@RestController
+public class SampleController {
+
+    @AutowiredFromBase(name = "sampleServiceImplNew")
+    private SampleService sampleServiceImplNew;
+
+    @AutowiredFromBase(name = "sampleServiceImpl")
+    private SampleService sampleServiceImpl;
+
+    @AutowiredFromBase
+    private List<SampleService> sampleServiceList;
+
+    @AutowiredFromBase
+    private Map<String, SampleService> sampleServiceMap;
+
+    @AutowiredFromBase
+    private AppService appService;
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+
+        sampleServiceImplNew.service();
+
+        sampleServiceImpl.service();
+
+        for (SampleService sampleService : sampleServiceList) {
+            sampleService.service();
+        }
+
+        for (String beanName : sampleServiceMap.keySet()) {
+            sampleServiceMap.get(beanName).service();
+        }
+
+        appService.getAppName();
+
+        return "hello to ark2 dynamic deploy";
+    }
+}
+

方式二:编程API SpringServiceFinder

+
@RestController
+public class SampleController {
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+
+        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
+        String result = sampleServiceImplFromFinder.service();
+        System.out.println(result);
+
+        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
+        for (String beanName : sampleServiceMapFromFinder.keySet()) {
+            String result1 = sampleServiceMapFromFinder.get(beanName).service();
+            System.out.println(result1);
+        }
+
+        return "hello to ark2 dynamic deploy";
+    }
+}
+

模块调用模块

+

参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

+

方式一:注解 @AutowiredFromBiz

+
@RestController
+public class SampleController {
+
+    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
+    private Provider studentProvider;
+
+    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
+    private Provider teacherProvider;
+
+    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
+    private List<Provider> providers;
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+
+        Result provide = studentProvider.provide(new Param());
+
+        Result provide1 = teacherProvider.provide(new Param());
+
+        for (Provider provider : providers) {
+            Result provide2 = provider.provide(new Param());
+        }
+
+        return "hello to ark2 dynamic deploy";
+    }
+}
+

方式二:编程API SpringServiceFinder

+
@RestController
+public class SampleController {
+
+    @RequestMapping(value = "/", method = RequestMethod.GET)
+    public String hello() {
+
+        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
+        Result result1 = teacherProvider1.provide(new Param());
+
+        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
+        for (String beanName : providerMap.keySet()) {
+            Result result2 = providerMap.get(beanName).provide(new Param());
+        }
+
+        return "hello to ark2 dynamic deploy";
+    }
+}
+

完整样例

+

SOFABoot 环境

+

请参考该文档

+
+
+ +
+ + + + + + + + + + + +
+ +

4.4.4 - 模块本地开发

+ +

Arkctl 工具安装

+

方法一:

+
    +
  1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
  2. +
  3. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。
  4. +
+

方法二:

+
    +
  1. 二进制列表 中下载对应的二进制并加入到本地 +path 中。
  2. +
+

本地快速部署

+

你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

+

场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

+

准备:

+
    +
  1. 在本地启动一个基座。
  2. +
  3. 打开一个模块项目仓库。
  4. +
+

执行命令:

+
# 需要在仓库的根目录下执行。
+# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
+arkctl deploy
+

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

+

场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

+

准备:

+
    +
  1. 在本地启动一个基座。
  2. +
  3. 准备一个构建好的 jar 包。
  4. +
+

执行命令:

+
arkctl deploy /path/to/your/pre/built/bundle-biz.jar
+

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

+

场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

+

准备:

+
    +
  1. 在远程已经运行起来的基座 pod。
  2. +
  3. 打开一个模块项目仓库。
  4. +
  5. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。
  6. +
+

执行命令:

+
# 需要在仓库的根目录下执行。
+# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
+arkctl deploy --pod {namespace}/{podName}
+

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

+

场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

+

准备:

+
    +
  1. 在本地启动一个基座。
  2. +
  3. 打开一个多模块 Maven 项目仓库。
  4. +
+

执行命令:

+
# 需要在仓库的根目录下执行。
+# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
+arkctl deploy --sub ./path/to/your/sub/module
+

命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

+

场景 5: 查询当前基座中已经部署的模块。

+

准备:

+
    +
  1. 在本地启动一个基座。
  2. +
+

执行命令:

+
arkctl status
+

场景 6: 查询远程 k8s 环境基座中已经部署的模块。

+

准备:

+
    +
  1. 在远程 k8s 环境启动一个基座。
  2. +
  3. 确保本地有 kube 证书以及有关权限。
  4. +
+

执行命令:

+
arkctl status --pod {namespace}/{name}
+
+
+ + + + + + + + + + + +
+ +

4.4.5 - 模块测试

+ +

本地调试

+

您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
Arklet HTTP 接口主要提供了以下能力:

+
    +
  1. 部署和卸载模块。
  2. +
  3. 查询所有已部署的模块信息。
  4. +
  5. 查询各项系统和业务指标。
  6. +
+

部署模块

+
curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
+

请求体样例:

+
{
+    "bizName": "test",
+    "bizVersion": "1.0.0",
+    // local path should start with file://, alse support remote url which can be downloaded
+    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
+}
+

部署成功返回结果样例:

+
{
+  "code":"SUCCESS",
+  "data":{
+    "bizInfos":[
+      {
+        "bizName":"dynamic-provider",
+        "bizState":"ACTIVATED",
+        "bizVersion":"1.0.0",
+        "declaredMode":true,
+        "identity":"dynamic-provider:1.0.0",
+        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+        "priority":100,
+        "webContextPath":"provider"
+      }
+    ],
+    "code":"SUCCESS",
+    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
+  }
+}
+

部署失败返回结果样例:

+
{
+  "code":"FAILED",
+  "data":{
+    "code":"REPEAT_BIZ",
+    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
+  }
+}
+

卸载模块

+
curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
+

请求体样例:

+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+

卸载成功返回结果样例:

+
{
+  "code":"SUCCESS"
+}
+

卸载失败返回结果样例:

+
{
+  "code":"FAILED",
+  "data":{
+    "code":"NOT_FOUND_BIZ",
+    "message":"Uninstall biz: test:1.0.0 not found."
+  }
+}
+

查询模块

+
curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
+

请求体样例:

+
{}
+

返回结果样例:

+
{
+  "code":"SUCCESS",
+  "data":[
+    {
+      "bizName":"dynamic-provider",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+      "webContextPath":"provider"
+    },
+    {
+      "bizName":"stock-mng",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"embed main",
+      "webContextPath":"/"
+    }
+  ]
+}
+

获取帮助

+

Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

+
curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
+

请求体样例:

+
{}
+

返回结果样例:

+
{
+    "code":"SUCCESS",
+    "data":[
+        {
+            "desc":"query all ark biz(including master biz)",
+            "id":"queryAllBiz"
+        },
+        {
+            "desc":"list all supported commands",
+            "id":"help"
+        },
+        {
+            "desc":"uninstall one ark biz",
+            "id":"uninstallBiz"
+        },
+        {
+            "desc":"switch one ark biz",
+            "id":"switchBiz"
+        },
+        {
+            "desc":"install one ark biz",
+            "id":"installBiz"
+        }
+    ]
+}
+

本地构建如何不改变模块版本号

+

添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

+
<profile>
+    <id>local</id>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.alipay.sofa</groupId>
+                <artifactId>sofa-ark-maven-plugin</artifactId>
+                <configuration>
+                    <finalName>${project.artifactId}-${project.version}</finalName>
+                    <bizVersion>${project.version}</bizVersion>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</profile>
+

单元测试

+

模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.4.6 - 复用基座拦截器

+ +

诉求

+

基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

+

解法

+

为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

+

步骤 1:

+

基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

+
public interface AnnotionService {
+    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
+}
+

步骤 2:

+

在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

+
@Service
+@SofaService(uniqueId = "facadeAroundHandler")
+public class FacadeAroundHandler implements AnnotionService {
+
+    private final static Logger LOG = LoggerConst.MY_LOGGER;
+
+    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+        log.info("开始执行")
+        joinPoint.proceed();
+        log.info("执行完成")
+    }
+}
+

步骤 3:

+

在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

+
//注意,这里不必申明成一个bean,不要加@Component或者@Service
+@Aspect
+public class FacadeAroundAspect {
+
+    @SofaReference(uniqueId = "facadeAroundHandler")
+    private AnnotionService facadeAroundHandler;
+
+    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
+    public void facadeAroundPointcut() {
+    }
+
+    @Around("facadeAroundPointcut()")
+    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+        return facadeAroundHandler.doAround(joinPoint);
+    }
+}
+

步骤 4:

+

使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

+
@Configuration
+public class MngAspectConfiguration {
+    @Bean
+    public FacadeAroundAspect facadeAroundAspect() {
+        return new FacadeAroundAspect();
+    }
+    @Bean
+    public EnvRouteAspect envRouteAspect() {
+        return new EnvRouteAspect();
+    }
+    @Bean
+    public FacadeAroundAspect facadeAroundAspect() {
+        return new FacadeAroundAspect();
+    }
+    
+}
+

步骤 5:

+

模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

+
@SpringBootApplication
+@ImportResource("classpath*:META-INF/spring/*.xml")
+@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
+public class ModuleBootstrapApplication {
+    public static void main(String[] args) {
+        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
+        	.web(WebApplicationType.NONE);
+        builder.build().run(args);
+    }
+}
+

+
+ +
+ + + + + + + + + + + +
+ +

4.4.7 - 复用基座数据源

+ +

建议

+

强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

+

SpringBoot 解法

+

在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
参考 demo:/sofa-serverless/samples/springboot-samples/db/mybatis/biz1

+

通过SpringBeanFinder.getBaseBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

+

+@Configuration
+@MapperScan(basePackages = "com.alipay.sofa.biz1.mapper", sqlSessionFactoryRef = "mysqlSqlFactory")
+@EnableTransactionManagement
+public class MybatisConfig {
+
+    //tips:不要初始化一个基座的DataSource,当模块被卸载的是,基座数据源会被销毁,transactionManager,transactionTemplate,mysqlSqlFactory被销毁没有问题
+
+    @Bean(name = "transactionManager")
+    public PlatformTransactionManager platformTransactionManager() {
+        return (PlatformTransactionManager) getBaseBean("transactionManager");
+    }
+
+    @Bean(name = "transactionTemplate")
+    public TransactionTemplate transactionTemplate() {
+        return (TransactionTemplate) getBaseBean("transactionTemplate");
+    }
+
+    @Bean(name = "mysqlSqlFactory")
+    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
+        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
+
+        DataSource dataSource = (DataSource) getBaseBean("dataSource");
+        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
+        mysqlSqlFactory.setDataSource(dataSource);
+        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
+                .getResources("classpath:mappers/*.xml"));
+        return mysqlSqlFactory;
+    }
+}
+

SOFABoot 解法

+

如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
image.png

+

+@Configuration
+@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
+@EnableTransactionManagement
+public class MybatisConfig {
+
+    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
+    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
+    
+    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
+
+    @Bean(name = "transactionManager")
+    public PlatformTransactionManager platformTransactionManager() {
+        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
+    }
+
+    @Bean(name = "transactionTemplate")
+    public TransactionTemplate transactionTemplate() {
+        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
+    }
+
+    @Bean(name = "mysqlSqlFactory")
+    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
+        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
+        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
+        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
+        mysqlSqlFactory.setDataSource(dataSource);
+        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
+                .getResources("classpath:mapper/*.xml"));
+        return mysqlSqlFactory;
+    }
+}
+

+
+ +
+ + + + + + + + + + + +
+ +

4.4.8 - 静态合并部署

+ +

介绍

+

SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 用户可以把 Biz 包统一放置在某个目录中,然后通过启动参数告知基座扫描这个目录,以此完成静态合并部署(详情见下描述)。如此,开发不需要考虑相互之间依赖冲突问题,Biz 之间则通过 @SofaService 和 @SofaReference 发布/引用 JVM 服务(SOFABoot,SpringBoot 还在建设中 -)进行交互。

步骤 1:模块应用打包成 Ark Biz

如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz -包,详见 Ark Biz 介绍。 Ark Biz 包使用 -Maven 插件 sofa-ark-maven-plugin 打包生成。


-<build>
-    <plugin>
-        <groupId>com.alipay.sofa</groupId>
-        <artifactId>sofa-ark-maven-plugin</artifactId>
-        <version>${sofa.ark.version}</version>
-        <executions>
-            <execution>
-                <id>default-cli</id>
-                <goals>
-                    <goal>repackage</goal>
-                </goals>
-            </execution>
-        </executions>
-    </plugin>
-</build>
-

步骤 2:将上述 jar 包移动到指定目录。

把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

mv /path/to/your/biz/jar /home/sofa-ark/biz/
-

步骤 3:启动基座,并通过 -D 参指定 biz 目录

java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
-

步骤 4:验证 Ark Biz(模块)启动

在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

telnet localhost 1234
-

然后执行如下命令查看模块列表:

biz -a
-

此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
上述操作可以通过 SOFAArk 静态合并部署样例 -体验。



4.4.9 - 模块中官方支持的中间件客户端

在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
注意,这里 “已经支持” 需要在基座 POM +)进行交互。

+

步骤 1:模块应用打包成 Ark Biz

+

如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz +包,详见 Ark Biz 介绍。 Ark Biz 包使用 +Maven 插件 sofa-ark-maven-plugin 打包生成。

+

+<build>
+    <plugin>
+        <groupId>com.alipay.sofa</groupId>
+        <artifactId>sofa-ark-maven-plugin</artifactId>
+        <version>${sofa.ark.version}</version>
+        <executions>
+            <execution>
+                <id>default-cli</id>
+                <goals>
+                    <goal>repackage</goal>
+                </goals>
+            </execution>
+        </executions>
+    </plugin>
+</build>
+

步骤 2:将上述 jar 包移动到指定目录。

+

把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

+
mv /path/to/your/biz/jar /home/sofa-ark/biz/
+

步骤 3:启动基座,并通过 -D 参指定 biz 目录

+
java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
+

步骤 4:验证 Ark Biz(模块)启动

+

在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

+
telnet localhost 1234
+

然后执行如下命令查看模块列表:

+
biz -a
+

此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
+上述操作可以通过 SOFAArk 静态合并部署样例 +体验。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.4.9 - 模块中官方支持的中间件客户端

+ +

在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
注意,这里 “已经支持” 需要在基座 POM 中引入相关客户端依赖(强烈建议使用 SpringBoot Starter 方式引入相关依赖),同时在模块 POM 中也引入相关依赖并设置 * -provided* 将依赖委托给基座加载。


中间件客户端版本号备注
JDK8.x
17.x
已经支持
SpringBoot>= 2.3.0 或 3.x已经支持
JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
SOFABoot>= 3.9.0 或 4.x已经支持
JMXN/A已经支持
需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
<dependency>
  <groupId>com.alipay.sofa.serverless</groupId>
  <artifactId>sofa-serverless-adapter-log4j2</artifactId>
  <version>${最新版 SOFAServerless 版本}</version>
  <scope>provided</scope> <!– 模块需要 provided –>
  </dependency>
基座和模块完整使用样例参见此处
slf4j-api1.x 且 >= 1.7已经支持
tomcat7.x、8.x、9.x
及以上均可; 10.x 暂不支持
已经支持
netty4.x已经支持
sofarpc>= 5.8.6已经支持
dubbo3.x已经支持
基座和模块完整使用样例及注意事项可参见此处
grpc1.x 且 >= 1.42已经支持
基座和模块完整使用样例及注意事项可参见此处
protobuf-java3.x 且 >= 3.17已经支持
基座和模块完整使用样例及注意事项可参见此处
apollo1.x 且 >= 1.6.0已经支持
基座和模块完整使用样例及注意事项可参见此处
kafka-client>= 2.8.0 或
>= 3.4.0
已经支持
基座和模块完整使用样例可参见此处
rocketmq4.x 且 >= 4.3.0已经支持
基座和模块完整使用样例可参见此处
jedis3.x已经支持
基座和模块完整使用样例可参见此处
xxl-job2.x 且 >= 2.1.0已经支持
需要在模块里声明为 compile 依赖独立使用
mybatis>= 2.2.2 或
>= 3.5.12
已经支持
基座和模块完整使用样例可参见此处
druid1.x已经支持
基座和模块完整使用样例可参见此处
mysql-connector-java8.x已经支持
基座和模块完整使用样例可参见此处
postgresql42.x 且 >= 42.3.8已经支持
mongodb4.6.1已经支持
基座和模块完整使用样例可参见此处
hibernate5.x 且 >= 5.6.15已经支持
j2cache任意已经支持
需要在模块里声明为 compile 依赖独立使用
opentracing0.x 且 >= 0.32.0已经支持
elasticsearch7.x 且 >= 7.6.2已经支持
jaspyt1.x 且 >= 1.9.3治理进行中
OKHttp-已经支持
需要放在基座里,请使用模块自动瘦身能力
io.kubernetes:client10.x 且 >= 10.0.0已经支持
net.java.dev.jna5.x 且 >= 5.12.1已经支持

4.4.10 - SOFAArk 关键用户文档

模块生命周期

Ark 事件机制

Ark 自身日志



4.5 - 模块运维

4.5.1 - 模块上线与下线

模块上线

在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
-

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

apiVersion: serverless.alipay.com/v1alpha1
-kind: ModuleDeployment
-metadata:
-  labels:
-    app.kubernetes.io/name: moduledeployment
-    app.kubernetes.io/instance: moduledeployment-sample
-    app.kubernetes.io/part-of: module-controller
-    app.kubernetes.io/managed-by: kustomize
-    app.kubernetes.io/created-by: module-controller
-  name: moduledeployment-sample
-spec:
-  baseDeploymentName: dynamic-stock-deployment
-  template:
-    spec:
-      module:
-        name: provider
-        version: '1.0.2'
-        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
-  replicas: 2
-  operationStrategy:  # 此处可自定义发布运维策略
-    upgradePolicy: installThenUninstall
-    needConfirm: true
-    useBeta: false
-    batchCount: 2
-  schedulingStrategy: # 此处可自定义调度策略
-    schedulingPolicy: Scatter
-

ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

模块下线

在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

kubectl delete yourmoduledeployment --namespace yournamespace
-

其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。



4.5.2 - 模块发布

模块发布

修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
-

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

apiVersion: serverless.alipay.com/v1alpha1
-kind: ModuleDeployment
-metadata:
-  labels:
-    app.kubernetes.io/name: moduledeployment
-    app.kubernetes.io/instance: moduledeployment-sample
-    app.kubernetes.io/part-of: module-controller
-    app.kubernetes.io/managed-by: kustomize
-    app.kubernetes.io/created-by: module-controller
-  name: moduledeployment-sample
-spec:
-  baseDeploymentName: dynamic-stock-deployment
-  template:
-    spec:
-      module:
-        name: provider
-        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
-        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
-        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
-  replicas: 2
-  operationStrategy:
-    upgradePolicy: install_then_uninstall
-    needConfirm: true
-    grayTimeBetweenBatchSeconds: 0
-    useBeta: false
-    batchCount: 2
-  schedulingStrategy:
-    schedulingPolicy: scatter
-

如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

模块回滚

重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。



4.5.3 - 基座和模块不兼容发布

步骤 1

修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

步骤 2

修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

步骤 3

使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。



4.5.4 - 模块扩缩容与替换

模块扩缩容

修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
-

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

apiVersion: serverless.alipay.com/v1alpha1
-kind: ModuleDeployment
-metadata:
-  labels:
-    app.kubernetes.io/name: moduledeployment
-    app.kubernetes.io/instance: moduledeployment-sample
-    app.kubernetes.io/part-of: module-controller
-    app.kubernetes.io/managed-by: kustomize
-    app.kubernetes.io/created-by: module-controller
-  name: moduledeployment-sample
-spec:
-  baseDeploymentName: dynamic-stock-deployment
-  template:
-    spec:
-      module:
-        name: provider
-        version: '1.0.2'
-        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
-  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
-  operationStrategy:
-    upgradePolicy: installThenUninstall
-    needConfirm: true
-    useBeta: false
-    batchCount: 2
-  schedulingStrategy: # 此处可自定义调度策略
-    schedulingPolicy: Scatter  
-

如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

模块替换

在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

kubectl delete yourmodule --namespace yournamespace
-

其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。



4.5.5 - 模块发布运维策略

运维策略

为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

字段名字段解释取值范围取值解释
batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
false 表示不启用 beta 分组
needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
false 表示不启用分组确认
grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次

调度策略

可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

保护机制

(正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

对等和非对等

您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。



4.5.6 - 独立使用 Arklet

Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处



4.5.7 - 模块信息查看

查看某个基座上所有安装的模块名称和状态

kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
-

kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
-

查看某个基座上所有安装的模块详细信息

kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
-

kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
-

替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

4.5.8 - 模块Service

ModuleService 简介

K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 +provided* 将依赖委托给基座加载。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
中间件客户端版本号备注
JDK8.x
17.x
已经支持
SpringBoot>= 2.3.0 或 3.x已经支持
JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
SpringBoot Cloud>= 2.7.x已经支持
基座和模块完整使用样例可参见此处
SOFABoot>= 3.9.0 或 4.x已经支持
JMXN/A已经支持
需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
<dependency>
  <groupId>com.alipay.sofa.serverless</groupId>
  <artifactId>sofa-serverless-adapter-log4j2</artifactId>
  <version>${最新版 SOFAServerless 版本}</version>
  <scope>provided</scope> <!– 模块需要 provided –>
  </dependency>
基座和模块完整使用样例参见此处
slf4j-api1.x 且 >= 1.7已经支持
tomcat7.x、8.x、9.x、10.x
及以上均可
已经支持
基座和模块完整使用样例可参见此处
netty4.x已经支持
基座和模块完整使用样例可参见此处
sofarpc>= 5.8.6已经支持
dubbo3.x已经支持
基座和模块完整使用样例及注意事项可参见此处
grpc1.x 且 >= 1.42已经支持
基座和模块完整使用样例及注意事项可参见此处
protobuf-java3.x 且 >= 3.17已经支持
基座和模块完整使用样例及注意事项可参见此处
apollo1.x 且 >= 1.6.0已经支持
基座和模块完整使用样例及注意事项可参见此处
nacos2.1.x已经支持
基座和模块完整使用样例及注意事项可参见此处
kafka-client>= 2.8.0 或
>= 3.4.0
已经支持
基座和模块完整使用样例可参见此处
rocketmq4.x 且 >= 4.3.0已经支持
基座和模块完整使用样例可参见此处
jedis3.x已经支持
基座和模块完整使用样例可参见此处
xxl-job2.x 且 >= 2.1.0已经支持
需要在模块里声明为 compile 依赖独立使用
mybatis>= 2.2.2 或
>= 3.5.12
已经支持
基座和模块完整使用样例可参见此处
druid1.x已经支持
基座和模块完整使用样例可参见此处
mysql-connector-java8.x已经支持
基座和模块完整使用样例可参见此处
postgresql42.x 且 >= 42.3.8已经支持
mongodb4.6.1已经支持
基座和模块完整使用样例可参见此处
hibernate5.x 且 >= 5.6.15已经支持
j2cache任意已经支持
需要在模块里声明为 compile 依赖独立使用
opentracing0.x 且 >= 0.32.0已经支持
elasticsearch7.x 且 >= 7.6.2已经支持
jaspyt1.x 且 >= 1.9.3已经支持
OKHttp-已经支持
需要放在基座里,请使用模块自动瘦身能力
io.kubernetes:client10.x 且 >= 10.0.0已经支持
net.java.dev.jna5.x 且 >= 5.12.1已经支持
prometheus-待验证支持
+ +
+ + + + + + + + + + + +
+ +

4.4.10 - SOFAArk 关键用户文档

+ +

模块生命周期

+Ark 事件机制

+Ark 自身日志

+
+
+ +
+ + + + + + + + + + + +
+ +

4.4.11 -

+ + +
+ + + + + + + + + + + + + + + +
+ +

4.5 - 模块运维

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

4.5.1 - 模块上线与下线

+ +

模块上线

+

在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

+
kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
+

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

+
apiVersion: serverless.alipay.com/v1alpha1
+kind: ModuleDeployment
+metadata:
+  labels:
+    app.kubernetes.io/name: moduledeployment
+    app.kubernetes.io/instance: moduledeployment-sample
+    app.kubernetes.io/part-of: module-controller
+    app.kubernetes.io/managed-by: kustomize
+    app.kubernetes.io/created-by: module-controller
+  name: moduledeployment-sample
+spec:
+  baseDeploymentName: dynamic-stock-deployment
+  template:
+    spec:
+      module:
+        name: provider
+        version: '1.0.2'
+        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
+  replicas: 2
+  operationStrategy:  # 此处可自定义发布运维策略
+    upgradePolicy: installThenUninstall
+    needConfirm: true
+    useBeta: false
+    batchCount: 2
+  schedulingStrategy: # 此处可自定义调度策略
+    schedulingPolicy: Scatter
+

ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

+

模块下线

+

在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

+
kubectl delete yourmoduledeployment --namespace yournamespace
+

其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.5.2 - 模块发布

+ +

模块发布

+

修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

+
kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
+

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

+
apiVersion: serverless.alipay.com/v1alpha1
+kind: ModuleDeployment
+metadata:
+  labels:
+    app.kubernetes.io/name: moduledeployment
+    app.kubernetes.io/instance: moduledeployment-sample
+    app.kubernetes.io/part-of: module-controller
+    app.kubernetes.io/managed-by: kustomize
+    app.kubernetes.io/created-by: module-controller
+  name: moduledeployment-sample
+spec:
+  baseDeploymentName: dynamic-stock-deployment
+  template:
+    spec:
+      module:
+        name: provider
+        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
+        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
+        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
+  replicas: 2
+  operationStrategy:
+    upgradePolicy: install_then_uninstall
+    needConfirm: true
+    grayTimeBetweenBatchSeconds: 0
+    useBeta: false
+    batchCount: 2
+  schedulingStrategy:
+    schedulingPolicy: scatter
+

如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

+

模块回滚

+

重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.5.3 - 基座和模块不兼容发布

+ +

步骤 1

+

修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

+

步骤 2

+

修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

+

步骤 3

+

使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.5.4 - 模块扩缩容与替换

+ +

模块扩缩容

+

修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

+
kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
+

其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

+
apiVersion: serverless.alipay.com/v1alpha1
+kind: ModuleDeployment
+metadata:
+  labels:
+    app.kubernetes.io/name: moduledeployment
+    app.kubernetes.io/instance: moduledeployment-sample
+    app.kubernetes.io/part-of: module-controller
+    app.kubernetes.io/managed-by: kustomize
+    app.kubernetes.io/created-by: module-controller
+  name: moduledeployment-sample
+spec:
+  baseDeploymentName: dynamic-stock-deployment
+  template:
+    spec:
+      module:
+        name: provider
+        version: '1.0.2'
+        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
+  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
+  operationStrategy:
+    upgradePolicy: installThenUninstall
+    needConfirm: true
+    useBeta: false
+    batchCount: 2
+  schedulingStrategy: # 此处可自定义调度策略
+    schedulingPolicy: Scatter  
+

如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

+

模块替换

+

在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

+
kubectl delete yourmodule --namespace yournamespace
+

其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.5.5 - 模块发布运维策略

+ +

运维策略

+

为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段名字段解释取值范围取值解释
batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
false 表示不启用 beta 分组
needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
false 表示不启用分组确认
grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次
+

调度策略

+

可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
+打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
+堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

+

保护机制

+

(正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

+

对等和非对等

+

您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
+非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
+对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。

+
+
+ +
+ + + + + + + + + + + +
+ +

4.5.6 - 独立使用 Arklet

+ +

Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处

+
+
+ +
+ + + + + + + + + + + +
+ +

4.5.7 - 模块信息查看

+ +

查看某个基座上所有安装的模块名称和状态

+
kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
+

+
kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
+

查看某个基座上所有安装的模块详细信息

+
kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
+

+
kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
+

替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

+ +
+ + + + + + + + + + + +
+ +

4.5.8 - 模块Service

+ +

ModuleService 简介

+

K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 模块也支持 Module 相关的 Service ,在模块发布时自动创建一个 service 来服务模块,将安装在一个或一组 Pod 的模块公开为网络服务。 -具体见:OperationStrategy.ServiceStrategy

apiVersion: serverless.alipay.com/v1alpha1
-kind: ModuleDeployment
-metadata:
-  labels:
-    app.kubernetes.io/name: moduledeployment
-    app.kubernetes.io/instance: moduledeployment-sample
-    app.kubernetes.io/part-of: module-controller
-    app.kubernetes.io/managed-by: kustomize
-    app.kubernetes.io/created-by: module-controller
-  name: moduledeployment-sample-provider
-spec:
-  baseDeploymentName: dynamic-stock-deployment
-  template:
-    spec:
-      module:
-        name: provider
-        version: '1.0.2'
-        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
-  replicas: 1
-  operationStrategy:
-    needConfirm: false
-    grayTimeBetweenBatchSeconds: 120
-    useBeta: false
-    batchCount: 1
-    upgradePolicy: install_then_uninstall
-    serviceStrategy:
-      enableModuleService: true
-      port: 8080
-      targetPort: 8080
-  schedulingStrategy:
-    schedulingPolicy: scatter
-

字段解释

OperationStrategy.ServiceStrategy 字段解释如下:

字段解释取值范围
EnableModuleService开启模块servicetrue or false
Port公开的端口1 到 65535
TargetPortpod上要访问的端口1 到 65535

示例

kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
-

自动创建的模块的 service

apiVersion: v1
-kind: Service
-metadata:
-  creationTimestamp: "2023-11-03T09:52:22Z"
-  name: dynamic-provider-service
-  namespace: default
-  resourceVersion: "28170024"
-  uid: 1f85e468-65e3-4181-b40e-48959a069df5
-spec:
-  clusterIP: 10.0.147.22
-  clusterIPs:
-  - 10.0.147.22
-  externalTrafficPolicy: Cluster
-  internalTrafficPolicy: Cluster
-  ipFamilies:
-  - IPv4
-  ipFamilyPolicy: SingleStack
-  ports:
-  - name: http
-    nodePort: 32232
-    port: 8080
-    protocol: TCP
-    targetPort: 8080
-  selector:
-    module.serverless.alipay.com/dynamic-provider: "true"
-  sessionAffinity: None
-  type: NodePort
-status:
-  loadBalancer: {}
-

4.5.9 - 所有 K8S 资源定义及部署方式

资源文件位置

  1. ModuleDeployment CRD 定义
  2. ModuleReplicaset CRD 定义
  3. ModuleTemplate CRD 定义
  4. Module CRD 定义
  5. Role 定义
  6. RBAC 定义
  7. ServiceAccount 定义
  8. ModuleController 部署定义

部署方式

使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。



5 - 参与社区

5.1 - 开放包容理念

核心价值观

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

  1. 社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

  2. 社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

  3. 社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

2023 年 OKR

O1 打造社区健康、有行业影响力的 Serverless 开源产品

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

O2 打造技术先进、效果显著的降本增效解决方案

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

RoadMap

  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • 2023.09 官网和完整用户手册上线。
  • 2023.10 新增 2+ 公司使用。
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。


5.2 - 交流渠道

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

SOFAServerless 社区交流与协作钉钉群:24970018417

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。

SOFAServerless 用户微信群


如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。

社区双周会

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
具体会议时间也可关注社区钉钉协作群(群号:24970018417)


每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。



5.3 - 贡献社区

5.3.1 - 本地开发测试

SOFAArk 和 Arklet

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

ModuleController

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
编译构建请在 module-controller 目录下执行:

go mod download   # if compile module-controller first time
-go build -a -o manager cmd/main.go  
-

单元测试执行请在 module-controller 目录下执行:

make test
-

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

Arkctl

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


5.3.2 - 完成第一次 PR 提交

认领或提交 Issue

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

获取源码

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

拉分支

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

  • 下载代码到本地,这一步您可以选择 git/https 方式:
git clone https://github.com/您的账号名/sofa-serverless.git
-
git clone https://github.com/您的账号名/sofa-ark.git
-
  • 拉分支准备修改代码:
git branch add_xxx_feature
-


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

  git branch -a
-

如果您想切换回主干,执行下面命令:

  git checkout -b master
-

如果您想切换回分支,执行下面命令:

  git checkout -b "branchName"
-

修改代码提交到本地

拉完分支后,就可以修改代码了。

修改代码注意事项

  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
mvn clean compile
-

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

  • 补充单元测试代码。
  • 确保新修改通过所有单元测试。
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
mvn clean test
-

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

make test
-

也可以通过 IDE 来辅助运行。

其它注意事项

  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • 对逻辑和功能不容易被理解的地方添加注释。
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。

修改完代码后,执行如下命令提交所有修改到本地:

git commit -am '添加xx功能'
-

提交代码到远程仓库

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

git push origin "branchname"
-

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

提交合并代码到主干的请求

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

PR 流水线校验

PR 流水线校验包括:

  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  3. 执行全部单元测试且必须全部通过。
  4. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  5. 检测提交的代码是否存在安全漏洞。
  6. 检测提交的代码是否符合基本代码规范。

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

Code Review

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

合并代码到主干

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。


5.3.3 - 文档、Issue、流程贡献

文档贡献

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 提交与回复贡献

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 模板

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

Question or Bug Report

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

### Describe the question or bug
-
-A clear and concise description of what the question or bug is.
-
-### Expected behavior
-
-A clear and concise description of what you expected to happen.
-
-### Actual behavior
-
-A clear and concise description of what actually happened.
-
-### Steps to reproduce
-
-Steps to reproduce the problem:
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-### Screenshots
-
-If applicable, add screenshots to help explain your problem.
-
-### Minimal yet complete reproducer code (or GitHub URL to code)
-
-### Environment
-
-- SOFAArk version:
-- JVM version (e.g. `java -version`):
-- OS version (e.g. `uname -a`):
-- Maven version:
-- IDE version:
-

Feature Request

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

流程贡献

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。



5.3.4 - 组织会议和运营布道

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. 与其他企业分享交流 SOFAServerless 的使用场景等。
  3. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  4. 其它运营方式。

5.4 - 社区角色与晋升

角色职责与晋升机制

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。

社区角色成员名单

SOFAArk

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Comittee)

glmapper

Committer

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

Contributor

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

Arklet

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

TomorJM

Committer

暂无

Contributor

glmapper
Lunarscave
lylingzhen

ModuleController

Maintainer

gold300jin

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

liu-657667
Charlie17Li
lylingzhen

Arkctl

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

暂无


5.6 - Arklet 技术文档

5.6.1 - Arklet 架构设计与接口设计

请参见 Arklet 源代码中的 README.md


5.6.2 - 如何发布 Arklet 版本

触发 github Action 发布到 snapshot staging

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
image.png

该 action 需要手动触发执行
image.png
执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

发布到 Release staging

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
点击左侧的 Staging Repositories:

搜索刚才记录的 ID:

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

仓库包同步与搜索

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。


5.7 - ModuleController 技术文档

5.7.1 - ModuleController 架构设计

介绍

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

基本架构

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。

功能清单和 RoadMap

  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)

5.7.2 - CRD 模型设计

CRD 模型对比

K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。

ModuleDeployment CRD 模型

image

Module CRD 模型

image

ModuleTemplate CRD 模型

image

ModuleReplicaSet CRD 模型

image


5.7.3 - 核心代码结构

image.png

image.png


核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。



5.7.4 - 模块生命周期

模块生命周期

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

image

模块状态机

image


5.7.5 - 核心流程时序图

模块首发

image

模块二发

image

模块下线

image

对等基座扩容

image

对等基座缩容

image

5.8 - Arkctl 技术文档

5.8.1 - Arkctl 技术文档

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


5.9 - 多模块运行时适配或最佳实践

5.9.1 - log4j2 的多模块化适配

为什么需要做适配

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

普通应用 log4j2 的初始化

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 -org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

该方法会根据 loggerContext 来判断是否已经初始化过了

这里在多模块下会存在问题一

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

  1. 获取到日志配置文件
  2. 解析日志配置文件里的变量值 -这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的

获取日志配置文件

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

解析日志配置值

配置文件里有一些变量,例如这些变量

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

预期多模块合并下的日志

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

多模块适配点

  1. getLoggerContext() 能拿到模块自身的 LoggerContext -

  2. 需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 -b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

模块改造方式

详细查看源码

5.9.2 - ehcache 的多模块化最佳实践

为什么需要最佳实践

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

最佳实践的几个要求

  1. 基座里必须引入 ehcache,模块里复用基座

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 -image.png

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, -image.png +具体见:OperationStrategy.ServiceStrategy

+
apiVersion: serverless.alipay.com/v1alpha1
+kind: ModuleDeployment
+metadata:
+  labels:
+    app.kubernetes.io/name: moduledeployment
+    app.kubernetes.io/instance: moduledeployment-sample
+    app.kubernetes.io/part-of: module-controller
+    app.kubernetes.io/managed-by: kustomize
+    app.kubernetes.io/created-by: module-controller
+  name: moduledeployment-sample-provider
+spec:
+  baseDeploymentName: dynamic-stock-deployment
+  template:
+    spec:
+      module:
+        name: provider
+        version: '1.0.2'
+        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
+  replicas: 1
+  operationStrategy:
+    needConfirm: false
+    grayTimeBetweenBatchSeconds: 120
+    useBeta: false
+    batchCount: 1
+    upgradePolicy: install_then_uninstall
+    serviceStrategy:
+      enableModuleService: true
+      port: 8080
+      targetPort: 8080
+  schedulingStrategy:
+    schedulingPolicy: scatter
+

字段解释

+

OperationStrategy.ServiceStrategy 字段解释如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
字段解释取值范围
EnableModuleService开启模块servicetrue or false
Port公开的端口1 到 65535
TargetPortpod上要访问的端口1 到 65535
+

示例

+
kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
+

自动创建的模块的 service

+
apiVersion: v1
+kind: Service
+metadata:
+  creationTimestamp: "2023-11-03T09:52:22Z"
+  name: dynamic-provider-service
+  namespace: default
+  resourceVersion: "28170024"
+  uid: 1f85e468-65e3-4181-b40e-48959a069df5
+spec:
+  clusterIP: 10.0.147.22
+  clusterIPs:
+  - 10.0.147.22
+  externalTrafficPolicy: Cluster
+  internalTrafficPolicy: Cluster
+  ipFamilies:
+  - IPv4
+  ipFamilyPolicy: SingleStack
+  ports:
+  - name: http
+    nodePort: 32232
+    port: 8080
+    protocol: TCP
+    targetPort: 8080
+  selector:
+    module.serverless.alipay.com/dynamic-provider: "true"
+  sessionAffinity: None
+  type: NodePort
+status:
+  loadBalancer: {}
+
+
+ + + + + + + + + + + +
+ +

4.5.9 - 所有 K8S 资源定义及部署方式

+ +

资源文件位置

+
    +
  1. ModuleDeployment CRD 定义
  2. +
  3. ModuleReplicaset CRD 定义
  4. +
  5. ModuleTemplate CRD 定义
  6. +
  7. Module CRD 定义
  8. +
  9. Role 定义
  10. +
  11. RBAC 定义
  12. +
  13. ServiceAccount 定义
  14. +
  15. ModuleController 部署定义
  16. +
+

部署方式

+

使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。

+
+
+
+ + + + + + + + + + + + + + + + + + + +
+ +

5 - 参与社区

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

5.1 - 开放包容理念

+ +

核心价值观

+

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

+
    +
  1. +

    社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

    +
  2. +
  3. +

    社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

    +
  4. +
  5. +

    社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

    +
  6. +
+

2023 年 OKR

+

O1 打造社区健康、有行业影响力的 Serverless 开源产品

+

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

+

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
+KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

+

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

+

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
+KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

+

O2 打造技术先进、效果显著的降本增效解决方案

+

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

+

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
+KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
+KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
+KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
+KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

+

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

+

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
+KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

+

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

+

RoadMap

+
    +
  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • +
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • +
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • +
  • 2023.09 官网和完整用户手册上线。
  • +
  • 2023.10 新增 2+ 公司使用。
  • +
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • +
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • +
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • +
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。
  • +
+
+
+
+ + + + + + + + + + + +
+ +

5.2 - 交流渠道

+ +

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

+

SOFAServerless 社区交流与协作钉钉群:24970018417

+

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。
+

+

SOFAServerless 用户微信群

+ +
+如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。
+
+

社区双周会

+

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
+入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
+钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
+具体会议时间也可关注社区钉钉协作群(群号:24970018417)

+
+

每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。

+
+
+ +
+ + + + + + + + + + + +
+ +

5.3 - 贡献社区

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

5.3.1 - 本地开发测试

+ +

SOFAArk 和 Arklet

+

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

+

ModuleController

+

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
+编译构建请在 module-controller 目录下执行:

+
go mod download   # if compile module-controller first time
+go build -a -o manager cmd/main.go  
+

单元测试执行请在 module-controller 目录下执行:

+
make test
+

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
+module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

+

Arkctl

+

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ +
+ + + + + + + + + + + +
+ +

5.3.2 - 完成第一次 PR 提交

+ +

认领或提交 Issue

+

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

+

获取源码

+

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

+

拉分支

+

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

+
    +
  • 下载代码到本地,这一步您可以选择 git/https 方式:
  • +
+
git clone https://github.com/您的账号名/sofa-serverless.git
+
git clone https://github.com/您的账号名/sofa-ark.git
+
    +
  • 拉分支准备修改代码:
  • +
+
git branch add_xxx_feature
+


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

+
  git branch -a
+

如果您想切换回主干,执行下面命令:

+
  git checkout -b master
+

如果您想切换回分支,执行下面命令:

+
  git checkout -b "branchName"
+

修改代码提交到本地

+

拉完分支后,就可以修改代码了。

+

修改代码注意事项

+
    +
  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
  • +
+
mvn clean compile
+

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

+
    +
  • 补充单元测试代码。
  • +
  • 确保新修改通过所有单元测试。
  • +
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
  • +
+
mvn clean test
+

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

+
make test
+

也可以通过 IDE 来辅助运行。

+

其它注意事项

+
    +
  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • +
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • +
  • 对逻辑和功能不容易被理解的地方添加注释。
  • +
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。
  • +
+

修改完代码后,执行如下命令提交所有修改到本地:

+
git commit -am '添加xx功能'
+

提交代码到远程仓库

+

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

+
git push origin "branchname"
+

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

+

提交合并代码到主干的请求

+

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

+

PR 流水线校验

+

PR 流水线校验包括:

+
    +
  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. +
  3. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  4. +
  5. 执行全部单元测试且必须全部通过。
  6. +
  7. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  8. +
  9. 检测提交的代码是否存在安全漏洞。
  10. +
  11. 检测提交的代码是否符合基本代码规范。
  12. +
+

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

+

Code Review

+

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

+

合并代码到主干

+

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。

+
+ +
+ + + + + + + + + + + +
+ +

5.3.3 - 文档、Issue、流程贡献

+ +

文档贡献

+

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 提交与回复贡献

+

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 模板

+

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

+

Question or Bug Report

+

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

+
### Describe the question or bug
+
+A clear and concise description of what the question or bug is.
+
+### Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+### Actual behavior
+
+A clear and concise description of what actually happened.
+
+### Steps to reproduce
+
+Steps to reproduce the problem:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+### Screenshots
+
+If applicable, add screenshots to help explain your problem.
+
+### Minimal yet complete reproducer code (or GitHub URL to code)
+
+### Environment
+
+- SOFAArk version:
+- JVM version (e.g. `java -version`):
+- OS version (e.g. `uname -a`):
+- Maven version:
+- IDE version:
+

Feature Request

+

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

+

流程贡献

+

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。

+
+
+ +
+ + + + + + + + + + + +
+ +

5.3.4 - 组织会议和运营布道

+ +

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

+
    +
  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. +
  3. 与其他企业分享交流 SOFAServerless 的使用场景等。
  4. +
  5. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  6. +
  7. 其它运营方式。
  8. +
+
+ +
+ + + + + + + + + + + + + + + +
+ +

5.4 - 社区角色与晋升

+ +

角色职责与晋升机制

+

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。
+

社区角色成员名单

+

SOFAArk

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Comittee)

+

glmapper

+

Committer

+

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

+

Contributor

+

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

+

Arklet

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Committee)

+

TomorJM

+

Committer

+

暂无

+

Contributor

+

glmapper
Lunarscave
lylingzhen

+

ModuleController

+

Maintainer

+

gold300jin

+

PMC (Project Management Committee)

+

暂无

+

Committer

+

暂无

+

Contributor

+

liu-657667
Charlie17Li
lylingzhen

+

Arkctl

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Committee)

+

暂无

+

Committer

+

暂无

+

Contributor

+

暂无

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

5.6 - Arklet 技术文档

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

5.6.1 - Arklet 架构设计与接口设计

+ +

概述

+

Arklet 为 SofaArk 基础和模块的交付提供了一个操作接口。有了 Arklet,Ark Biz 的发布和操作可以轻松灵活地进行。

+

Arklet 是由 ArkletComponent 内部构建的

+

image

+
    +
  • ApiClient: 负责与外界交互的核心组件
  • +
  • CommandService: Arklet 对外暴露能力指令定义和扩展
  • +
  • OperationService: Ark Biz 与 SofaArk 交互,进行添加、删除、修改和封装基本能力
  • +
  • HealthService: 基于健康和稳定性,计算基础、Biz、系统等其他指标
  • +
+

他们之间的协作如图所示 +overview

+

当然,您也可以通过实现 ArkletComponent 接口来扩展 Arklet 的组件功能

+

命令扩展

+

Arklet 外部公开了指令 API,并通过每个 API 映射的 CommandHandler 内部处理指令。

+
+

CommandHandler 相关的扩展属于 CommandService 组件的统一管理

+
+

您可以通过继承 AbstractCommandHandler 来自定义扩展命令

+

内置命令 API

+

以下所有的指令 api 都使用 POST(application/json) 请求格式访问 arklet

+

启用了 http 协议,默认端口是 1238

+
+

您可以设置 sofa.serverless.arklet.http.port JVM 启动参数覆盖默认端口

+
+

查询支持的命令

+
    +
  • URL: 127.0.0.1:1238/help
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+    "code":"SUCCESS",
+    "data":[
+        {
+            "desc":"query all ark biz(including master biz)",
+            "id":"queryAllBiz"
+        },
+        {
+            "desc":"list all supported commands",
+            "id":"help"
+        },
+        {
+            "desc":"uninstall one ark biz",
+            "id":"uninstallBiz"
+        },
+        {
+            "desc":"switch one ark biz",
+            "id":"switchBiz"
+        },
+        {
+            "desc":"install one ark biz",
+            "id":"installBiz"
+        }
+    ]
+}
+

安装一个 biz

+
    +
  • URL: 127.0.0.1:1238/installBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName": "test",
+    "bizVersion": "1.0.0",
+    // local path should start with file://, alse support remote url which can be downloaded
+    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
+}
+
    +
  • 输出样例(成功):
  • +
+
{
+  "code":"SUCCESS",
+  "data":{
+    "bizInfos":[
+      {
+        "bizName":"dynamic-provider",
+        "bizState":"ACTIVATED",
+        "bizVersion":"1.0.0",
+        "declaredMode":true,
+        "identity":"dynamic-provider:1.0.0",
+        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+        "priority":100,
+        "webContextPath":"provider"
+      }
+    ],
+    "code":"SUCCESS",
+    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
+  }
+}
+

-输出样例(失败):

+
{
+  "code":"FAILED",
+  "data":{
+    "code":"REPEAT_BIZ",
+    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
+  }
+}
+

卸载模块

+
    +
  • URL: 127.0.0.1:1238/uninstallBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+

-输出样例(成功):

+
{
+  "code":"SUCCESS"
+}
+
    +
  • 输出样例(失败):
  • +
+
{
+  "code":"FAILED",
+  "data":{
+    "code":"NOT_FOUND_BIZ",
+    "message":"Uninstall biz: test:1.0.0 not found."
+  }
+}
+

Switch a biz

+
    +
  • URL: 127.0.0.1:1238/switchBiz
  • +
  • 输出样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS"
+}
+

查询所有 Biz

+
    +
  • URL: 127.0.0.1:1238/queryAllBiz
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS",
+  "data":[
+    {
+      "bizName":"dynamic-provider",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+      "webContextPath":"provider"
+    },
+    {
+      "bizName":"stock-mng",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"embed main",
+      "webContextPath":"/"
+    }
+  ]
+}
+

查询健康状况

+
    +
  • URL: 127.0.0.1:1238/health
  • +
+

以下根据不同的输入参数,获取到不同的状态信息

+

查询健康状况

+
    +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {
+        "max non heap memory(M)": -9.5367431640625E-7,
+        "java version": "1.8.0_331",
+        "max memory(M)": 885.5,
+        "max heap memory(M)": 885.5,
+        "used heap memory(M)": 137.14127349853516,
+        "used non heap memory(M)": 62.54662322998047,
+        "loaded class count": 10063,
+        "init non heap memory(M)": 2.4375,
+        "total memory(M)": 174.5,
+        "free memory(M)": 37.358726501464844,
+        "unload class count": 0,
+        "total class count": 10063,
+        "committed heap memory(M)": 174.5,
+        "java home": "****\\jre",
+        "init heap memory(M)": 64.0,
+        "committed non heap memory(M)": 66.203125,
+        "run time(s)": 34.432
+      },
+      "cpu": {
+        "count": 4,
+        "total used (%)": 131749.0,
+        "type": "****",
+        "user used (%)": 9.926451054656962,
+        "free (%)": 81.46475495070172,
+        "system used (%)": 6.249762806548817
+      },
+      "masterBizInfo": {
+        "webContextPath": "/",
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0"
+      },
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/2.2.3-SNAPSHOT/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        },
+        {
+          "artifactId": "runtime-sofa-boot-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.runtime.ark.plugin.SofaRuntimeActivator",
+          "pluginName": "runtime-sofa-boot-plugin",
+          "pluginUrl": "file:/****/runtime-sofa-boot-plugin-3.11.0.jar!/",
+          "pluginVersion": "3.11.0"
+        }
+      ],
+      "masterBizHealth": {
+        "readinessState": "ACCEPTING_TRAFFIC"
+      },
+      "bizListInfo": [
+        {
+          "bizName": "bookstore-manager",
+          "bizState": "ACTIVATED",
+          "bizVersion": "1.0.0",
+          "webContextPath": "/"
+        }
+      ]
+    }
+  }
+}
+

查询系统健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "system",
+  // [OPTIONAL] if metrics is null -> query all system health info
+  "metrics": ["cpu", "jvm"]
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {...},
+      "cpu": {...},
+//      "masterBizHealth": {...}
+    }
+  }
+}
+

查询模块健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "biz",
+  // [OPTIONAL] if moduleName is null and moduleVersion is null -> query all biz
+  "moduleName": "bookstore-manager",
+  // [OPTIONAL] if moduleVersion is null -> query all biz named moduleName
+  "moduleVersion": "1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "bizInfo": {
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0",
+        "webContextPath": "/"
+      }
+//      "bizListInfo": [
+//        {
+//          "bizName": "bookstore-manager",
+//          "bizState": "ACTIVATED",
+//          "bizVersion": "1.0.0",
+//          "webContextPath": "/"
+//        }
+//      ]
+    }
+  }
+}
+

查询插件健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "plugin",
+  // [OPTIONAL] if moduleName is null -> query all biz
+  "moduleName": "web-ark-plugin"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        }
+      ]
+    }
+  }
+}
+

使用端点查询健康状况

+

使用端点获取 k8s 模块的健康信息

+

默认配置

+
    +
  • 端点暴露包括:*
  • +
  • 端点基本路径:/
  • +
  • 端点服务器端口:8080
  • +
+

http 代码结果

+
    +
  • HEALTHY(200):如果所有健康指标都是健康的,获取健康信息
  • +
  • UNHEALTHY(400):一旦健康指标不健康,获取健康信息
  • +
  • ENDPOINT_NOT_FOUND(404):找不到端点路径或参数
  • +
  • ENDPOINT_PROCESS_INTERNAL_ERROR(500):获取健康过程中抛出错误
  • +
+

查询所有健康信息

+
curl 127.0.0.1:8080/arkletHealth
+
    +
  • 输出样例
  • +
+
{   
+    "healthy": true,
+    "code": 200,    
+    "codeType": "HEALTHY",    
+    "data": {        
+        "jvm": {...},        
+        "masterBizHealth": {...},        
+        "cpu": {...},        
+        "masterBizInfo": {...},        
+        "bizListInfo": [...],        
+        "pluginListInfo": [...]    
+    }
+}  
+

查询所有 biz/plugin 健康信息

+
curl: 127.0.0.1:8080/arkletHealth/{moduleType} (moduleType 必须在 ['biz', 'plugin'])
+
    +
  • 输出样例
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizListInfo": [...],  
+       // "pluginListInfo": [...]      
+   }
+}  
+

查询单个 biz/plugin 健康信息

+
curl 127.0.0.1:8080/arkletHealth/{moduleType}/moduleName/moduleVersion (moduleType must in ['biz', 'plugin'])
+
    +
  • 输出样例:
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizInfo": {...},  
+       // "pluginInfo": {...}      
+   }
+}
+

+ +
+ + + + + + + + + + + +
+ +

5.6.2 - 如何发布 Arklet 版本

+ +

触发 github Action 发布到 snapshot staging

+

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
+image.png

+

该 action 需要手动触发执行
+image.png
+执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

+

发布到 Release staging

+

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
+点击左侧的 Staging Repositories:
+

+

搜索刚才记录的 ID:
+

+

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

+

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

+

仓库包同步与搜索

+

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。

+
+ +
+ + + + + + + + + + + + + + + +
+ +

5.7 - ModuleController 技术文档

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

5.7.1 - ModuleController 架构设计

+ +

介绍

+

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

+

基本架构

+

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。
+

+

功能清单和 RoadMap

+
    +
  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • +
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • +
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • +
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • +
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • +
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)
  • +
+
+ +
+ + + + + + + + + + + +
+ +

5.7.2 - CRD 模型设计

+ +

CRD 模型对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。
+

ModuleDeployment CRD 模型

+

image

+

Module CRD 模型

+

image

+

ModuleTemplate CRD 模型

+

image

+

ModuleReplicaSet CRD 模型

+

image

+
+ +
+ + + + + + + + + + + +
+ +

5.7.3 - 核心代码结构

+ +

image.png

+

image.png

+
+

核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。

+
+
+ +
+ + + + + + + + + + + +
+ +

5.7.4 - 模块生命周期

+ +

模块生命周期

+

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

+

image

+

模块状态机

+

image

+
+ +
+ + + + + + + + + + + +
+ +

5.7.5 - 核心流程时序图

+ +

模块首发

+

image

+

模块二发

+

image

+

模块下线

+

image

+

对等基座扩容

+

image

+

对等基座缩容

+

image +

+ +
+ + + + + + + + + + + + + + + +
+ +

5.8 - Arkctl 技术文档

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

5.8.1 - Arkctl 技术文档

+ +

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ +
+ + + + + + + + + + + + + + + +
+ +

5.9 - 多模块运行时适配或最佳实践

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

5.9.1 - log4j2 的多模块化适配

+ +

为什么需要做适配

+

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

+

普通应用 log4j2 的初始化

+

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 +org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

+

+

该方法会根据 loggerContext 来判断是否已经初始化过了

+
+

这里在多模块下会存在问题一

+

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

+
+

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

+
    +
  1. 获取到日志配置文件
  2. +
  3. 解析日志配置文件里的变量值 +这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的
  4. +
+

获取日志配置文件

+

+

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

+

+

解析日志配置值

+

配置文件里有一些变量,例如这些变量

+

+

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

+ + + + + + + + + + + + + + + + + + + + +
变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中
+

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

+

预期多模块合并下的日志

+

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

+

多模块适配点

+
    +
  1. +

    getLoggerContext() 能拿到模块自身的 LoggerContext +

    +
  2. +
  3. +

    需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    +

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 +b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

    +
  4. +
+

模块改造方式

+

详细查看源码

+ +
+ + + + + + + + + + + +
+ +

5.9.2 - ehcache 的多模块化最佳实践

+ +

为什么需要最佳实践

+

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

+

最佳实践的几个要求

+
    +
  1. 基座里必须引入 ehcache,模块里复用基座
  2. +
+

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 +image.png

+

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, +image.png 如果 net.sf.ehcache.CacheManager 是。这里会走到 java native 方法上做判断,从当前类所在的 ClassLoader 里查找 net.sf.ehcache.CacheManager 类,所以基座里必须引入这个依赖,否则会报 ClassNotFound 的错误。 -image.png

  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, -image.png -image.png +image.png

+
    +
  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)
  2. +
+

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, +image.png +image.png 这里在 new 对象时,需要先获得对象所属类的 CacheManager 是基座的 CacheManager。这里也不能讲 CacheManager 由模块 compile 引入,否则会出现一个类由多个不同 ClassLoader 引入导致的问题。 -image.png

所以结论是,这里需要全部委托给基座加载。

最佳实践的方式

  1. 模块 ehcache 排包瘦身委托给基座加载
  2. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  3. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
 <plugin>
-    <groupId>com.google.code.maven-replacer-plugin</groupId>
-    <artifactId>replacer</artifactId>
-    <version>1.5.3</version>
-    <executions>
-        <!-- 打包前进行替换 -->
-        <execution>
-            <phase>prepare-package</phase>
-            <goals>
-                <goal>replace</goal>
-            </goals>
-        </execution>
-    </executions>
-    <configuration>
-        <!-- 自动识别到项目target文件夹 -->
-        <basedir>${build.directory}</basedir>
-        <!-- 替换的文件所在目录规则 -->
-        <includes>
-            <include>classes/j2cache/*.properties</include>
-        </includes>
-        <replacements>
-            <replacement>
-                <token>ehcache.ehcache.name=f6-cache</token>
-                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
-            </replacement>
-
-        </replacements>
-    </configuration>
-</plugin>
-
  1. 需要把 FactoryBean 的 shared 设置成 false
@Bean
-    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
-        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
-
-        // 需要把 factoryBean 的 share 属性设置成 false
-        factoryBean.setShared(true);
-//        factoryBean.setShared(false);
-        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
-        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
-        return factoryBean;
-    }
-

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 -image.png -image.png

最佳实践的样例

样例工程请参考这里

6 - 常见 FAQ

6.1 - 如果模块独立引入 SpringBoot 框架部分会怎样?

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

CreateSpringFactoriesInstances

image.png

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener
-

所以模块框架这部分需要委托给基座加载。



- - \ No newline at end of file +image.png

+

所以结论是,这里需要全部委托给基座加载。

+

最佳实践的方式

+
    +
  1. 模块 ehcache 排包瘦身委托给基座加载
  2. +
  3. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  4. +
  5. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
  6. +
+
 <plugin>
+    <groupId>com.google.code.maven-replacer-plugin</groupId>
+    <artifactId>replacer</artifactId>
+    <version>1.5.3</version>
+    <executions>
+        <!-- 打包前进行替换 -->
+        <execution>
+            <phase>prepare-package</phase>
+            <goals>
+                <goal>replace</goal>
+            </goals>
+        </execution>
+    </executions>
+    <configuration>
+        <!-- 自动识别到项目target文件夹 -->
+        <basedir>${build.directory}</basedir>
+        <!-- 替换的文件所在目录规则 -->
+        <includes>
+            <include>classes/j2cache/*.properties</include>
+        </includes>
+        <replacements>
+            <replacement>
+                <token>ehcache.ehcache.name=f6-cache</token>
+                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
+            </replacement>
+
+        </replacements>
+    </configuration>
+</plugin>
+
    +
  1. 需要把 FactoryBean 的 shared 设置成 false
  2. +
+
@Bean
+    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
+        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
+
+        // 需要把 factoryBean 的 share 属性设置成 false
+        factoryBean.setShared(true);
+//        factoryBean.setShared(false);
+        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
+        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
+        return factoryBean;
+    }
+

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 +image.png +image.png

+

最佳实践的样例

+

样例工程请参考这里

+ +
+ + + + + + + + + + + +
+ +

5.9.3 -

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

6 - 常见 FAQ

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

6.1 - 常见问题列表

+ +

问题 1-1:模块 compile 引入 springboot 依赖,模块安装时报错

+
java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+
解决方式
+

模块需要做好瘦身,参考这里:模块瘦身

+

问题 1-2:模块安装找不到 ServerlessApplicationListener

+

报错信息如下:

+
com.alipay.sofa.ark.exception.ArkLoaderException: [ArkBiz Loader] module1:1.0-SNAPSHOT : can not load class: com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+
解决方式
+

请在模块里面添加如下依赖:

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>0.5.5</version>
+</dependency>
+

或者升级 sofa-serverless 版本到最新版本

+

问题 1-3: 通过 go install 无法安装 arkctl

+

执行如下命令,报错

+
go install serverless.alipay.com/sofa-serverless/v1/arkctl@latest
+

报错信息如下:

+
go: serverless.alipay.com/sofa-serverless/v1/arkctl@latest: module serverless.alipay.com/sofa-serverless/v1/arkctl: Get "https://proxy.golang.org/serverless.alipay.com/sofa-serverless/v1/arkctl/@v/list": dial tcp 142.251.42.241:443: i/o timeout
+
解决方式
+

arkctl 是作为 sofa-serverless 子目录的方式存在的,所以没法直接 go get,可以从这下面下载执行文件, 请参考安装 arkctl

+

问题 1-4:模块安装报 Master biz environment is null

+

解决方式,升级 sofa-serverless 版本到最新版本

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>${最新版本号}</version>
+</dependency>
+

问题 1-5:模块静态合并部署无法从制定的目录里找到模块包

+

解决方式,升级 sofa-serverless 版本到最新版本

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>${最新版本号}</version>
+</dependency>
+
+
+ + + + + + + + + + + +
+ +

6.2 - 如果模块独立引入 SpringBoot 框架部分会怎样?

+ +

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

+

CreateSpringFactoriesInstances

+

image.png

+

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

+
Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener
+

所以模块框架这部分需要委托给基座加载。

+
+
+ +
+ + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/_print/index.html b/docs/public/docs/contribution-guidelines/_print/index.html index 821c715ec..881f2878a 100644 --- a/docs/public/docs/contribution-guidelines/_print/index.html +++ b/docs/public/docs/contribution-guidelines/_print/index.html @@ -1,110 +1,1887 @@ -参与社区 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +参与社区 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - 开放包容理念

核心价值观

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

  1. 社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

  2. 社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

  3. 社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

2023 年 OKR

O1 打造社区健康、有行业影响力的 Serverless 开源产品

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

O2 打造技术先进、效果显著的降本增效解决方案

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

RoadMap

  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • 2023.09 官网和完整用户手册上线。
  • 2023.10 新增 2+ 公司使用。
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。


2 - 交流渠道

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

SOFAServerless 社区交流与协作钉钉群:24970018417

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。

SOFAServerless 用户微信群


如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。

社区双周会

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
具体会议时间也可关注社区钉钉协作群(群号:24970018417)


每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。



3 - 贡献社区

3.1 - 本地开发测试

SOFAArk 和 Arklet

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

ModuleController

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
编译构建请在 module-controller 目录下执行:

go mod download   # if compile module-controller first time
-go build -a -o manager cmd/main.go  
-

单元测试执行请在 module-controller 目录下执行:

make test
-

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

Arkctl

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


3.2 - 完成第一次 PR 提交

认领或提交 Issue

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

获取源码

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

拉分支

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

  • 下载代码到本地,这一步您可以选择 git/https 方式:
git clone https://github.com/您的账号名/sofa-serverless.git
-
git clone https://github.com/您的账号名/sofa-ark.git
-
  • 拉分支准备修改代码:
git branch add_xxx_feature
-


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

  git branch -a
-

如果您想切换回主干,执行下面命令:

  git checkout -b master
-

如果您想切换回分支,执行下面命令:

  git checkout -b "branchName"
-

修改代码提交到本地

拉完分支后,就可以修改代码了。

修改代码注意事项

  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
mvn clean compile
-

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

  • 补充单元测试代码。
  • 确保新修改通过所有单元测试。
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
mvn clean test
-

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

make test
-

也可以通过 IDE 来辅助运行。

其它注意事项

  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • 对逻辑和功能不容易被理解的地方添加注释。
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。

修改完代码后,执行如下命令提交所有修改到本地:

git commit -am '添加xx功能'
-

提交代码到远程仓库

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

git push origin "branchname"
-

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

提交合并代码到主干的请求

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

PR 流水线校验

PR 流水线校验包括:

  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  3. 执行全部单元测试且必须全部通过。
  4. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  5. 检测提交的代码是否存在安全漏洞。
  6. 检测提交的代码是否符合基本代码规范。

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

Code Review

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

合并代码到主干

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。


3.3 - 文档、Issue、流程贡献

文档贡献

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 提交与回复贡献

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 模板

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

Question or Bug Report

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

### Describe the question or bug
-
-A clear and concise description of what the question or bug is.
-
-### Expected behavior
-
-A clear and concise description of what you expected to happen.
-
-### Actual behavior
-
-A clear and concise description of what actually happened.
-
-### Steps to reproduce
-
-Steps to reproduce the problem:
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-### Screenshots
-
-If applicable, add screenshots to help explain your problem.
-
-### Minimal yet complete reproducer code (or GitHub URL to code)
-
-### Environment
-
-- SOFAArk version:
-- JVM version (e.g. `java -version`):
-- OS version (e.g. `uname -a`):
-- Maven version:
-- IDE version:
-

Feature Request

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

流程贡献

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。



3.4 - 组织会议和运营布道

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. 与其他企业分享交流 SOFAServerless 的使用场景等。
  3. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  4. 其它运营方式。

4 - 社区角色与晋升

角色职责与晋升机制

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。

社区角色成员名单

SOFAArk

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Comittee)

glmapper

Committer

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

Contributor

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

Arklet

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

TomorJM

Committer

暂无

Contributor

glmapper
Lunarscave
lylingzhen

ModuleController

Maintainer

gold300jin

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

liu-657667
Charlie17Li
lylingzhen

Arkctl

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

暂无


6 - Arklet 技术文档

6.1 - Arklet 架构设计与接口设计

请参见 Arklet 源代码中的 README.md


6.2 - 如何发布 Arklet 版本

触发 github Action 发布到 snapshot staging

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
image.png

该 action 需要手动触发执行
image.png
执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

发布到 Release staging

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
点击左侧的 Staging Repositories:

搜索刚才记录的 ID:

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

仓库包同步与搜索

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。


7 - ModuleController 技术文档

7.1 - ModuleController 架构设计

介绍

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

基本架构

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。

功能清单和 RoadMap

  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)

7.2 - CRD 模型设计

CRD 模型对比

K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。

ModuleDeployment CRD 模型

image

Module CRD 模型

image

ModuleTemplate CRD 模型

image

ModuleReplicaSet CRD 模型

image


7.3 - 核心代码结构

image.png

image.png


核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。



7.4 - 模块生命周期

模块生命周期

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

image

模块状态机

image


7.5 - 核心流程时序图

模块首发

image

模块二发

image

模块下线

image

对等基座扩容

image

对等基座缩容

image

8 - Arkctl 技术文档

8.1 - Arkctl 技术文档

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


9 - 多模块运行时适配或最佳实践

9.1 - log4j2 的多模块化适配

为什么需要做适配

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

普通应用 log4j2 的初始化

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 -org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

该方法会根据 loggerContext 来判断是否已经初始化过了

这里在多模块下会存在问题一

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

  1. 获取到日志配置文件
  2. 解析日志配置文件里的变量值 -这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的

获取日志配置文件

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

解析日志配置值

配置文件里有一些变量,例如这些变量

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

预期多模块合并下的日志

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

多模块适配点

  1. getLoggerContext() 能拿到模块自身的 LoggerContext -

  2. 需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 -b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

模块改造方式

详细查看源码

9.2 - ehcache 的多模块化最佳实践

为什么需要最佳实践

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

最佳实践的几个要求

  1. 基座里必须引入 ehcache,模块里复用基座

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 -image.png

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, -image.png + + + +

+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

参与社区

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - 开放包容理念

+ +

核心价值观

+

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

+
    +
  1. +

    社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

    +
  2. +
  3. +

    社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

    +
  4. +
  5. +

    社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

    +
  6. +
+

2023 年 OKR

+

O1 打造社区健康、有行业影响力的 Serverless 开源产品

+

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

+

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
+KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

+

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

+

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
+KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

+

O2 打造技术先进、效果显著的降本增效解决方案

+

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

+

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
+KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
+KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
+KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
+KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

+

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

+

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
+KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

+

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

+

RoadMap

+
    +
  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • +
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • +
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • +
  • 2023.09 官网和完整用户手册上线。
  • +
  • 2023.10 新增 2+ 公司使用。
  • +
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • +
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • +
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • +
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。
  • +
+
+
+
+ + + + + + + + + + + +
+ +

2 - 交流渠道

+ +

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

+

SOFAServerless 社区交流与协作钉钉群:24970018417

+

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。
+

+

SOFAServerless 用户微信群

+ +
+如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。
+
+

社区双周会

+

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
+入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
+钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
+具体会议时间也可关注社区钉钉协作群(群号:24970018417)

+
+

每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。

+
+
+ +
+ + + + + + + + + + + +
+ +

3 - 贡献社区

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

3.1 - 本地开发测试

+ +

SOFAArk 和 Arklet

+

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

+

ModuleController

+

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
+编译构建请在 module-controller 目录下执行:

+
go mod download   # if compile module-controller first time
+go build -a -o manager cmd/main.go  
+

单元测试执行请在 module-controller 目录下执行:

+
make test
+

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
+module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

+

Arkctl

+

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ +
+ + + + + + + + + + + +
+ +

3.2 - 完成第一次 PR 提交

+ +

认领或提交 Issue

+

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

+

获取源码

+

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

+

拉分支

+

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

+
    +
  • 下载代码到本地,这一步您可以选择 git/https 方式:
  • +
+
git clone https://github.com/您的账号名/sofa-serverless.git
+
git clone https://github.com/您的账号名/sofa-ark.git
+
    +
  • 拉分支准备修改代码:
  • +
+
git branch add_xxx_feature
+


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

+
  git branch -a
+

如果您想切换回主干,执行下面命令:

+
  git checkout -b master
+

如果您想切换回分支,执行下面命令:

+
  git checkout -b "branchName"
+

修改代码提交到本地

+

拉完分支后,就可以修改代码了。

+

修改代码注意事项

+
    +
  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
  • +
+
mvn clean compile
+

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

+
    +
  • 补充单元测试代码。
  • +
  • 确保新修改通过所有单元测试。
  • +
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
  • +
+
mvn clean test
+

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

+
make test
+

也可以通过 IDE 来辅助运行。

+

其它注意事项

+
    +
  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • +
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • +
  • 对逻辑和功能不容易被理解的地方添加注释。
  • +
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。
  • +
+

修改完代码后,执行如下命令提交所有修改到本地:

+
git commit -am '添加xx功能'
+

提交代码到远程仓库

+

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

+
git push origin "branchname"
+

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

+

提交合并代码到主干的请求

+

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

+

PR 流水线校验

+

PR 流水线校验包括:

+
    +
  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. +
  3. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  4. +
  5. 执行全部单元测试且必须全部通过。
  6. +
  7. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  8. +
  9. 检测提交的代码是否存在安全漏洞。
  10. +
  11. 检测提交的代码是否符合基本代码规范。
  12. +
+

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

+

Code Review

+

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

+

合并代码到主干

+

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。

+
+ +
+ + + + + + + + + + + +
+ +

3.3 - 文档、Issue、流程贡献

+ +

文档贡献

+

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 提交与回复贡献

+

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 模板

+

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

+

Question or Bug Report

+

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

+
### Describe the question or bug
+
+A clear and concise description of what the question or bug is.
+
+### Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+### Actual behavior
+
+A clear and concise description of what actually happened.
+
+### Steps to reproduce
+
+Steps to reproduce the problem:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+### Screenshots
+
+If applicable, add screenshots to help explain your problem.
+
+### Minimal yet complete reproducer code (or GitHub URL to code)
+
+### Environment
+
+- SOFAArk version:
+- JVM version (e.g. `java -version`):
+- OS version (e.g. `uname -a`):
+- Maven version:
+- IDE version:
+

Feature Request

+

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

+

流程贡献

+

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。

+
+
+ +
+ + + + + + + + + + + +
+ +

3.4 - 组织会议和运营布道

+ +

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

+
    +
  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. +
  3. 与其他企业分享交流 SOFAServerless 的使用场景等。
  4. +
  5. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  6. +
  7. 其它运营方式。
  8. +
+
+ +
+ + + + + + + + + + + + + + + +
+ +

4 - 社区角色与晋升

+ +

角色职责与晋升机制

+

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。
+

社区角色成员名单

+

SOFAArk

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Comittee)

+

glmapper

+

Committer

+

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

+

Contributor

+

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

+

Arklet

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Committee)

+

TomorJM

+

Committer

+

暂无

+

Contributor

+

glmapper
Lunarscave
lylingzhen

+

ModuleController

+

Maintainer

+

gold300jin

+

PMC (Project Management Committee)

+

暂无

+

Committer

+

暂无

+

Contributor

+

liu-657667
Charlie17Li
lylingzhen

+

Arkctl

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Committee)

+

暂无

+

Committer

+

暂无

+

Contributor

+

暂无

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

6 - Arklet 技术文档

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

6.1 - Arklet 架构设计与接口设计

+ +

概述

+

Arklet 为 SofaArk 基础和模块的交付提供了一个操作接口。有了 Arklet,Ark Biz 的发布和操作可以轻松灵活地进行。

+

Arklet 是由 ArkletComponent 内部构建的

+

image

+
    +
  • ApiClient: 负责与外界交互的核心组件
  • +
  • CommandService: Arklet 对外暴露能力指令定义和扩展
  • +
  • OperationService: Ark Biz 与 SofaArk 交互,进行添加、删除、修改和封装基本能力
  • +
  • HealthService: 基于健康和稳定性,计算基础、Biz、系统等其他指标
  • +
+

他们之间的协作如图所示 +overview

+

当然,您也可以通过实现 ArkletComponent 接口来扩展 Arklet 的组件功能

+

命令扩展

+

Arklet 外部公开了指令 API,并通过每个 API 映射的 CommandHandler 内部处理指令。

+
+

CommandHandler 相关的扩展属于 CommandService 组件的统一管理

+
+

您可以通过继承 AbstractCommandHandler 来自定义扩展命令

+

内置命令 API

+

以下所有的指令 api 都使用 POST(application/json) 请求格式访问 arklet

+

启用了 http 协议,默认端口是 1238

+
+

您可以设置 sofa.serverless.arklet.http.port JVM 启动参数覆盖默认端口

+
+

查询支持的命令

+
    +
  • URL: 127.0.0.1:1238/help
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+    "code":"SUCCESS",
+    "data":[
+        {
+            "desc":"query all ark biz(including master biz)",
+            "id":"queryAllBiz"
+        },
+        {
+            "desc":"list all supported commands",
+            "id":"help"
+        },
+        {
+            "desc":"uninstall one ark biz",
+            "id":"uninstallBiz"
+        },
+        {
+            "desc":"switch one ark biz",
+            "id":"switchBiz"
+        },
+        {
+            "desc":"install one ark biz",
+            "id":"installBiz"
+        }
+    ]
+}
+

安装一个 biz

+
    +
  • URL: 127.0.0.1:1238/installBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName": "test",
+    "bizVersion": "1.0.0",
+    // local path should start with file://, alse support remote url which can be downloaded
+    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
+}
+
    +
  • 输出样例(成功):
  • +
+
{
+  "code":"SUCCESS",
+  "data":{
+    "bizInfos":[
+      {
+        "bizName":"dynamic-provider",
+        "bizState":"ACTIVATED",
+        "bizVersion":"1.0.0",
+        "declaredMode":true,
+        "identity":"dynamic-provider:1.0.0",
+        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+        "priority":100,
+        "webContextPath":"provider"
+      }
+    ],
+    "code":"SUCCESS",
+    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
+  }
+}
+

-输出样例(失败):

+
{
+  "code":"FAILED",
+  "data":{
+    "code":"REPEAT_BIZ",
+    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
+  }
+}
+

卸载模块

+
    +
  • URL: 127.0.0.1:1238/uninstallBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+

-输出样例(成功):

+
{
+  "code":"SUCCESS"
+}
+
    +
  • 输出样例(失败):
  • +
+
{
+  "code":"FAILED",
+  "data":{
+    "code":"NOT_FOUND_BIZ",
+    "message":"Uninstall biz: test:1.0.0 not found."
+  }
+}
+

Switch a biz

+
    +
  • URL: 127.0.0.1:1238/switchBiz
  • +
  • 输出样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS"
+}
+

查询所有 Biz

+
    +
  • URL: 127.0.0.1:1238/queryAllBiz
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS",
+  "data":[
+    {
+      "bizName":"dynamic-provider",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+      "webContextPath":"provider"
+    },
+    {
+      "bizName":"stock-mng",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"embed main",
+      "webContextPath":"/"
+    }
+  ]
+}
+

查询健康状况

+
    +
  • URL: 127.0.0.1:1238/health
  • +
+

以下根据不同的输入参数,获取到不同的状态信息

+

查询健康状况

+
    +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {
+        "max non heap memory(M)": -9.5367431640625E-7,
+        "java version": "1.8.0_331",
+        "max memory(M)": 885.5,
+        "max heap memory(M)": 885.5,
+        "used heap memory(M)": 137.14127349853516,
+        "used non heap memory(M)": 62.54662322998047,
+        "loaded class count": 10063,
+        "init non heap memory(M)": 2.4375,
+        "total memory(M)": 174.5,
+        "free memory(M)": 37.358726501464844,
+        "unload class count": 0,
+        "total class count": 10063,
+        "committed heap memory(M)": 174.5,
+        "java home": "****\\jre",
+        "init heap memory(M)": 64.0,
+        "committed non heap memory(M)": 66.203125,
+        "run time(s)": 34.432
+      },
+      "cpu": {
+        "count": 4,
+        "total used (%)": 131749.0,
+        "type": "****",
+        "user used (%)": 9.926451054656962,
+        "free (%)": 81.46475495070172,
+        "system used (%)": 6.249762806548817
+      },
+      "masterBizInfo": {
+        "webContextPath": "/",
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0"
+      },
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/2.2.3-SNAPSHOT/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        },
+        {
+          "artifactId": "runtime-sofa-boot-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.runtime.ark.plugin.SofaRuntimeActivator",
+          "pluginName": "runtime-sofa-boot-plugin",
+          "pluginUrl": "file:/****/runtime-sofa-boot-plugin-3.11.0.jar!/",
+          "pluginVersion": "3.11.0"
+        }
+      ],
+      "masterBizHealth": {
+        "readinessState": "ACCEPTING_TRAFFIC"
+      },
+      "bizListInfo": [
+        {
+          "bizName": "bookstore-manager",
+          "bizState": "ACTIVATED",
+          "bizVersion": "1.0.0",
+          "webContextPath": "/"
+        }
+      ]
+    }
+  }
+}
+

查询系统健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "system",
+  // [OPTIONAL] if metrics is null -> query all system health info
+  "metrics": ["cpu", "jvm"]
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {...},
+      "cpu": {...},
+//      "masterBizHealth": {...}
+    }
+  }
+}
+

查询模块健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "biz",
+  // [OPTIONAL] if moduleName is null and moduleVersion is null -> query all biz
+  "moduleName": "bookstore-manager",
+  // [OPTIONAL] if moduleVersion is null -> query all biz named moduleName
+  "moduleVersion": "1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "bizInfo": {
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0",
+        "webContextPath": "/"
+      }
+//      "bizListInfo": [
+//        {
+//          "bizName": "bookstore-manager",
+//          "bizState": "ACTIVATED",
+//          "bizVersion": "1.0.0",
+//          "webContextPath": "/"
+//        }
+//      ]
+    }
+  }
+}
+

查询插件健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "plugin",
+  // [OPTIONAL] if moduleName is null -> query all biz
+  "moduleName": "web-ark-plugin"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        }
+      ]
+    }
+  }
+}
+

使用端点查询健康状况

+

使用端点获取 k8s 模块的健康信息

+

默认配置

+
    +
  • 端点暴露包括:*
  • +
  • 端点基本路径:/
  • +
  • 端点服务器端口:8080
  • +
+

http 代码结果

+
    +
  • HEALTHY(200):如果所有健康指标都是健康的,获取健康信息
  • +
  • UNHEALTHY(400):一旦健康指标不健康,获取健康信息
  • +
  • ENDPOINT_NOT_FOUND(404):找不到端点路径或参数
  • +
  • ENDPOINT_PROCESS_INTERNAL_ERROR(500):获取健康过程中抛出错误
  • +
+

查询所有健康信息

+
curl 127.0.0.1:8080/arkletHealth
+
    +
  • 输出样例
  • +
+
{   
+    "healthy": true,
+    "code": 200,    
+    "codeType": "HEALTHY",    
+    "data": {        
+        "jvm": {...},        
+        "masterBizHealth": {...},        
+        "cpu": {...},        
+        "masterBizInfo": {...},        
+        "bizListInfo": [...],        
+        "pluginListInfo": [...]    
+    }
+}  
+

查询所有 biz/plugin 健康信息

+
curl: 127.0.0.1:8080/arkletHealth/{moduleType} (moduleType 必须在 ['biz', 'plugin'])
+
    +
  • 输出样例
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizListInfo": [...],  
+       // "pluginListInfo": [...]      
+   }
+}  
+

查询单个 biz/plugin 健康信息

+
curl 127.0.0.1:8080/arkletHealth/{moduleType}/moduleName/moduleVersion (moduleType must in ['biz', 'plugin'])
+
    +
  • 输出样例:
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizInfo": {...},  
+       // "pluginInfo": {...}      
+   }
+}
+

+ +
+ + + + + + + + + + + +
+ +

6.2 - 如何发布 Arklet 版本

+ +

触发 github Action 发布到 snapshot staging

+

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
+image.png

+

该 action 需要手动触发执行
+image.png
+执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

+

发布到 Release staging

+

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
+点击左侧的 Staging Repositories:
+

+

搜索刚才记录的 ID:
+

+

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

+

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

+

仓库包同步与搜索

+

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。

+
+ +
+ + + + + + + + + + + + + + + +
+ +

7 - ModuleController 技术文档

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

7.1 - ModuleController 架构设计

+ +

介绍

+

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

+

基本架构

+

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。
+

+

功能清单和 RoadMap

+
    +
  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • +
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • +
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • +
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • +
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • +
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)
  • +
+
+ +
+ + + + + + + + + + + +
+ +

7.2 - CRD 模型设计

+ +

CRD 模型对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。
+

ModuleDeployment CRD 模型

+

image

+

Module CRD 模型

+

image

+

ModuleTemplate CRD 模型

+

image

+

ModuleReplicaSet CRD 模型

+

image

+
+ +
+ + + + + + + + + + + +
+ +

7.3 - 核心代码结构

+ +

image.png

+

image.png

+
+

核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。

+
+
+ +
+ + + + + + + + + + + +
+ +

7.4 - 模块生命周期

+ +

模块生命周期

+

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

+

image

+

模块状态机

+

image

+
+ +
+ + + + + + + + + + + +
+ +

7.5 - 核心流程时序图

+ +

模块首发

+

image

+

模块二发

+

image

+

模块下线

+

image

+

对等基座扩容

+

image

+

对等基座缩容

+

image +

+ +
+ + + + + + + + + + + + + + + +
+ +

8 - Arkctl 技术文档

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

8.1 - Arkctl 技术文档

+ +

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ +
+ + + + + + + + + + + + + + + +
+ +

9 - 多模块运行时适配或最佳实践

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

9.1 - log4j2 的多模块化适配

+ +

为什么需要做适配

+

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

+

普通应用 log4j2 的初始化

+

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 +org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

+

+

该方法会根据 loggerContext 来判断是否已经初始化过了

+
+

这里在多模块下会存在问题一

+

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

+
+

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

+
    +
  1. 获取到日志配置文件
  2. +
  3. 解析日志配置文件里的变量值 +这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的
  4. +
+

获取日志配置文件

+

+

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

+

+

解析日志配置值

+

配置文件里有一些变量,例如这些变量

+

+

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

+ + + + + + + + + + + + + + + + + + + + +
变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中
+

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

+

预期多模块合并下的日志

+

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

+

多模块适配点

+
    +
  1. +

    getLoggerContext() 能拿到模块自身的 LoggerContext +

    +
  2. +
  3. +

    需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    +

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 +b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

    +
  4. +
+

模块改造方式

+

详细查看源码

+ +
+ + + + + + + + + + + +
+ +

9.2 - ehcache 的多模块化最佳实践

+ +

为什么需要最佳实践

+

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

+

最佳实践的几个要求

+
    +
  1. 基座里必须引入 ehcache,模块里复用基座
  2. +
+

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 +image.png

+

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, +image.png 如果 net.sf.ehcache.CacheManager 是。这里会走到 java native 方法上做判断,从当前类所在的 ClassLoader 里查找 net.sf.ehcache.CacheManager 类,所以基座里必须引入这个依赖,否则会报 ClassNotFound 的错误。 -image.png

  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, -image.png -image.png +image.png

+
    +
  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)
  2. +
+

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, +image.png +image.png 这里在 new 对象时,需要先获得对象所属类的 CacheManager 是基座的 CacheManager。这里也不能讲 CacheManager 由模块 compile 引入,否则会出现一个类由多个不同 ClassLoader 引入导致的问题。 -image.png

所以结论是,这里需要全部委托给基座加载。

最佳实践的方式

  1. 模块 ehcache 排包瘦身委托给基座加载
  2. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  3. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
 <plugin>
-    <groupId>com.google.code.maven-replacer-plugin</groupId>
-    <artifactId>replacer</artifactId>
-    <version>1.5.3</version>
-    <executions>
-        <!-- 打包前进行替换 -->
-        <execution>
-            <phase>prepare-package</phase>
-            <goals>
-                <goal>replace</goal>
-            </goals>
-        </execution>
-    </executions>
-    <configuration>
-        <!-- 自动识别到项目target文件夹 -->
-        <basedir>${build.directory}</basedir>
-        <!-- 替换的文件所在目录规则 -->
-        <includes>
-            <include>classes/j2cache/*.properties</include>
-        </includes>
-        <replacements>
-            <replacement>
-                <token>ehcache.ehcache.name=f6-cache</token>
-                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
-            </replacement>
-
-        </replacements>
-    </configuration>
-</plugin>
-
  1. 需要把 FactoryBean 的 shared 设置成 false
@Bean
-    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
-        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
-
-        // 需要把 factoryBean 的 share 属性设置成 false
-        factoryBean.setShared(true);
-//        factoryBean.setShared(false);
-        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
-        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
-        return factoryBean;
-    }
-

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 -image.png -image.png

最佳实践的样例

样例工程请参考这里

- - \ No newline at end of file +image.png

+

所以结论是,这里需要全部委托给基座加载。

+

最佳实践的方式

+
    +
  1. 模块 ehcache 排包瘦身委托给基座加载
  2. +
  3. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  4. +
  5. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
  6. +
+
 <plugin>
+    <groupId>com.google.code.maven-replacer-plugin</groupId>
+    <artifactId>replacer</artifactId>
+    <version>1.5.3</version>
+    <executions>
+        <!-- 打包前进行替换 -->
+        <execution>
+            <phase>prepare-package</phase>
+            <goals>
+                <goal>replace</goal>
+            </goals>
+        </execution>
+    </executions>
+    <configuration>
+        <!-- 自动识别到项目target文件夹 -->
+        <basedir>${build.directory}</basedir>
+        <!-- 替换的文件所在目录规则 -->
+        <includes>
+            <include>classes/j2cache/*.properties</include>
+        </includes>
+        <replacements>
+            <replacement>
+                <token>ehcache.ehcache.name=f6-cache</token>
+                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
+            </replacement>
+
+        </replacements>
+    </configuration>
+</plugin>
+
    +
  1. 需要把 FactoryBean 的 shared 设置成 false
  2. +
+
@Bean
+    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
+        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
+
+        // 需要把 factoryBean 的 share 属性设置成 false
+        factoryBean.setShared(true);
+//        factoryBean.setShared(false);
+        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
+        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
+        return factoryBean;
+    }
+

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 +image.png +image.png

+

最佳实践的样例

+

样例工程请参考这里

+ +
+ + + + + + + + + + + +
+ +

9.3 -

+ + +
+ + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/arkctl/_print/index.html b/docs/public/docs/contribution-guidelines/arkctl/_print/index.html index fb64a92dc..dea17d493 100644 --- a/docs/public/docs/contribution-guidelines/arkctl/_print/index.html +++ b/docs/public/docs/contribution-guidelines/arkctl/_print/index.html @@ -1,8 +1,257 @@ -Arkctl 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Arkctl 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

这是本节的多页打印视图。 -点击此处打印.

返回本页常规视图.

Arkctl 技术文档

1 - Arkctl 技术文档

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

Arkctl 技术文档

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - Arkctl 技术文档

+ +

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html b/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html index 3efdcfe31..4ffde8e7e 100644 --- a/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html +++ b/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html @@ -1,12 +1,482 @@ -Arkctl 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +Arkctl 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Arkctl 技术文档

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


-

最后修改 October 27, 2023: docs fix (37243ef7)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

Arkctl 技术文档

+ + +

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 27, 2023: docs fix (37243ef7) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/arkctl/index.html b/docs/public/docs/contribution-guidelines/arkctl/index.html index 74a89db2d..1d47413dd 100644 --- a/docs/public/docs/contribution-guidelines/arkctl/index.html +++ b/docs/public/docs/contribution-guidelines/arkctl/index.html @@ -1,12 +1,494 @@ -Arkctl 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Arkctl 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Arkctl 技术文档

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

Arkctl 技术文档

+ + + +
+ + + + + + + + +
+ + +
+
+ Arkctl 技术文档 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/arklet/_print/index.html b/docs/public/docs/contribution-guidelines/arklet/_print/index.html index 292458e3d..b39a24f63 100644 --- a/docs/public/docs/contribution-guidelines/arklet/_print/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/_print/index.html @@ -1,8 +1,693 @@ -Arklet 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Arklet 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - Arklet 架构设计与接口设计

请参见 Arklet 源代码中的 README.md


2 - 如何发布 Arklet 版本

触发 github Action 发布到 snapshot staging

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
image.png

该 action 需要手动触发执行
image.png
执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

发布到 Release staging

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
点击左侧的 Staging Repositories:

搜索刚才记录的 ID:

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

仓库包同步与搜索

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。


- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

Arklet 技术文档

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - Arklet 架构设计与接口设计

+ +

概述

+

Arklet 为 SofaArk 基础和模块的交付提供了一个操作接口。有了 Arklet,Ark Biz 的发布和操作可以轻松灵活地进行。

+

Arklet 是由 ArkletComponent 内部构建的

+

image

+
    +
  • ApiClient: 负责与外界交互的核心组件
  • +
  • CommandService: Arklet 对外暴露能力指令定义和扩展
  • +
  • OperationService: Ark Biz 与 SofaArk 交互,进行添加、删除、修改和封装基本能力
  • +
  • HealthService: 基于健康和稳定性,计算基础、Biz、系统等其他指标
  • +
+

他们之间的协作如图所示 +overview

+

当然,您也可以通过实现 ArkletComponent 接口来扩展 Arklet 的组件功能

+

命令扩展

+

Arklet 外部公开了指令 API,并通过每个 API 映射的 CommandHandler 内部处理指令。

+
+

CommandHandler 相关的扩展属于 CommandService 组件的统一管理

+
+

您可以通过继承 AbstractCommandHandler 来自定义扩展命令

+

内置命令 API

+

以下所有的指令 api 都使用 POST(application/json) 请求格式访问 arklet

+

启用了 http 协议,默认端口是 1238

+
+

您可以设置 sofa.serverless.arklet.http.port JVM 启动参数覆盖默认端口

+
+

查询支持的命令

+
    +
  • URL: 127.0.0.1:1238/help
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+    "code":"SUCCESS",
+    "data":[
+        {
+            "desc":"query all ark biz(including master biz)",
+            "id":"queryAllBiz"
+        },
+        {
+            "desc":"list all supported commands",
+            "id":"help"
+        },
+        {
+            "desc":"uninstall one ark biz",
+            "id":"uninstallBiz"
+        },
+        {
+            "desc":"switch one ark biz",
+            "id":"switchBiz"
+        },
+        {
+            "desc":"install one ark biz",
+            "id":"installBiz"
+        }
+    ]
+}
+

安装一个 biz

+
    +
  • URL: 127.0.0.1:1238/installBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName": "test",
+    "bizVersion": "1.0.0",
+    // local path should start with file://, alse support remote url which can be downloaded
+    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
+}
+
    +
  • 输出样例(成功):
  • +
+
{
+  "code":"SUCCESS",
+  "data":{
+    "bizInfos":[
+      {
+        "bizName":"dynamic-provider",
+        "bizState":"ACTIVATED",
+        "bizVersion":"1.0.0",
+        "declaredMode":true,
+        "identity":"dynamic-provider:1.0.0",
+        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+        "priority":100,
+        "webContextPath":"provider"
+      }
+    ],
+    "code":"SUCCESS",
+    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
+  }
+}
+

-输出样例(失败):

+
{
+  "code":"FAILED",
+  "data":{
+    "code":"REPEAT_BIZ",
+    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
+  }
+}
+

卸载模块

+
    +
  • URL: 127.0.0.1:1238/uninstallBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+

-输出样例(成功):

+
{
+  "code":"SUCCESS"
+}
+
    +
  • 输出样例(失败):
  • +
+
{
+  "code":"FAILED",
+  "data":{
+    "code":"NOT_FOUND_BIZ",
+    "message":"Uninstall biz: test:1.0.0 not found."
+  }
+}
+

Switch a biz

+
    +
  • URL: 127.0.0.1:1238/switchBiz
  • +
  • 输出样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS"
+}
+

查询所有 Biz

+
    +
  • URL: 127.0.0.1:1238/queryAllBiz
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS",
+  "data":[
+    {
+      "bizName":"dynamic-provider",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+      "webContextPath":"provider"
+    },
+    {
+      "bizName":"stock-mng",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"embed main",
+      "webContextPath":"/"
+    }
+  ]
+}
+

查询健康状况

+
    +
  • URL: 127.0.0.1:1238/health
  • +
+

以下根据不同的输入参数,获取到不同的状态信息

+

查询健康状况

+
    +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {
+        "max non heap memory(M)": -9.5367431640625E-7,
+        "java version": "1.8.0_331",
+        "max memory(M)": 885.5,
+        "max heap memory(M)": 885.5,
+        "used heap memory(M)": 137.14127349853516,
+        "used non heap memory(M)": 62.54662322998047,
+        "loaded class count": 10063,
+        "init non heap memory(M)": 2.4375,
+        "total memory(M)": 174.5,
+        "free memory(M)": 37.358726501464844,
+        "unload class count": 0,
+        "total class count": 10063,
+        "committed heap memory(M)": 174.5,
+        "java home": "****\\jre",
+        "init heap memory(M)": 64.0,
+        "committed non heap memory(M)": 66.203125,
+        "run time(s)": 34.432
+      },
+      "cpu": {
+        "count": 4,
+        "total used (%)": 131749.0,
+        "type": "****",
+        "user used (%)": 9.926451054656962,
+        "free (%)": 81.46475495070172,
+        "system used (%)": 6.249762806548817
+      },
+      "masterBizInfo": {
+        "webContextPath": "/",
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0"
+      },
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/2.2.3-SNAPSHOT/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        },
+        {
+          "artifactId": "runtime-sofa-boot-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.runtime.ark.plugin.SofaRuntimeActivator",
+          "pluginName": "runtime-sofa-boot-plugin",
+          "pluginUrl": "file:/****/runtime-sofa-boot-plugin-3.11.0.jar!/",
+          "pluginVersion": "3.11.0"
+        }
+      ],
+      "masterBizHealth": {
+        "readinessState": "ACCEPTING_TRAFFIC"
+      },
+      "bizListInfo": [
+        {
+          "bizName": "bookstore-manager",
+          "bizState": "ACTIVATED",
+          "bizVersion": "1.0.0",
+          "webContextPath": "/"
+        }
+      ]
+    }
+  }
+}
+

查询系统健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "system",
+  // [OPTIONAL] if metrics is null -> query all system health info
+  "metrics": ["cpu", "jvm"]
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {...},
+      "cpu": {...},
+//      "masterBizHealth": {...}
+    }
+  }
+}
+

查询模块健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "biz",
+  // [OPTIONAL] if moduleName is null and moduleVersion is null -> query all biz
+  "moduleName": "bookstore-manager",
+  // [OPTIONAL] if moduleVersion is null -> query all biz named moduleName
+  "moduleVersion": "1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "bizInfo": {
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0",
+        "webContextPath": "/"
+      }
+//      "bizListInfo": [
+//        {
+//          "bizName": "bookstore-manager",
+//          "bizState": "ACTIVATED",
+//          "bizVersion": "1.0.0",
+//          "webContextPath": "/"
+//        }
+//      ]
+    }
+  }
+}
+

查询插件健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "plugin",
+  // [OPTIONAL] if moduleName is null -> query all biz
+  "moduleName": "web-ark-plugin"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        }
+      ]
+    }
+  }
+}
+

使用端点查询健康状况

+

使用端点获取 k8s 模块的健康信息

+

默认配置

+
    +
  • 端点暴露包括:*
  • +
  • 端点基本路径:/
  • +
  • 端点服务器端口:8080
  • +
+

http 代码结果

+
    +
  • HEALTHY(200):如果所有健康指标都是健康的,获取健康信息
  • +
  • UNHEALTHY(400):一旦健康指标不健康,获取健康信息
  • +
  • ENDPOINT_NOT_FOUND(404):找不到端点路径或参数
  • +
  • ENDPOINT_PROCESS_INTERNAL_ERROR(500):获取健康过程中抛出错误
  • +
+

查询所有健康信息

+
curl 127.0.0.1:8080/arkletHealth
+
    +
  • 输出样例
  • +
+
{   
+    "healthy": true,
+    "code": 200,    
+    "codeType": "HEALTHY",    
+    "data": {        
+        "jvm": {...},        
+        "masterBizHealth": {...},        
+        "cpu": {...},        
+        "masterBizInfo": {...},        
+        "bizListInfo": [...],        
+        "pluginListInfo": [...]    
+    }
+}  
+

查询所有 biz/plugin 健康信息

+
curl: 127.0.0.1:8080/arkletHealth/{moduleType} (moduleType 必须在 ['biz', 'plugin'])
+
    +
  • 输出样例
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizListInfo": [...],  
+       // "pluginListInfo": [...]      
+   }
+}  
+

查询单个 biz/plugin 健康信息

+
curl 127.0.0.1:8080/arkletHealth/{moduleType}/moduleName/moduleVersion (moduleType must in ['biz', 'plugin'])
+
    +
  • 输出样例:
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizInfo": {...},  
+       // "pluginInfo": {...}      
+   }
+}
+

+ +
+ + + + + + + + + + + +
+ +

2 - 如何发布 Arklet 版本

+ +

触发 github Action 发布到 snapshot staging

+

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
+image.png

+

该 action 需要手动触发执行
+image.png
+执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

+

发布到 Release staging

+

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
+点击左侧的 Staging Repositories:
+

+

搜索刚才记录的 ID:
+

+

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

+

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

+

仓库包同步与搜索

+

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。

+
+ +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/arklet/architecture/index.html b/docs/public/docs/contribution-guidelines/arklet/architecture/index.html index 2478e2de9..8f5a12ac9 100644 --- a/docs/public/docs/contribution-guidelines/arklet/architecture/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/architecture/index.html @@ -1,12 +1,936 @@ -Arklet 架构设计与接口设计 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +Arklet 架构设计与接口设计 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Arklet 架构设计与接口设计

请参见 Arklet 源代码中的 README.md


-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

Arklet 架构设计与接口设计

+ + +

概述

+

Arklet 为 SofaArk 基础和模块的交付提供了一个操作接口。有了 Arklet,Ark Biz 的发布和操作可以轻松灵活地进行。

+

Arklet 是由 ArkletComponent 内部构建的

+

image

+
    +
  • ApiClient: 负责与外界交互的核心组件
  • +
  • CommandService: Arklet 对外暴露能力指令定义和扩展
  • +
  • OperationService: Ark Biz 与 SofaArk 交互,进行添加、删除、修改和封装基本能力
  • +
  • HealthService: 基于健康和稳定性,计算基础、Biz、系统等其他指标
  • +
+

他们之间的协作如图所示 +overview

+

当然,您也可以通过实现 ArkletComponent 接口来扩展 Arklet 的组件功能

+

命令扩展

+

Arklet 外部公开了指令 API,并通过每个 API 映射的 CommandHandler 内部处理指令。

+
+

CommandHandler 相关的扩展属于 CommandService 组件的统一管理

+
+

您可以通过继承 AbstractCommandHandler 来自定义扩展命令

+

内置命令 API

+

以下所有的指令 api 都使用 POST(application/json) 请求格式访问 arklet

+

启用了 http 协议,默认端口是 1238

+
+

您可以设置 sofa.serverless.arklet.http.port JVM 启动参数覆盖默认端口

+
+

查询支持的命令

+
    +
  • URL: 127.0.0.1:1238/help
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+    "code":"SUCCESS",
+    "data":[
+        {
+            "desc":"query all ark biz(including master biz)",
+            "id":"queryAllBiz"
+        },
+        {
+            "desc":"list all supported commands",
+            "id":"help"
+        },
+        {
+            "desc":"uninstall one ark biz",
+            "id":"uninstallBiz"
+        },
+        {
+            "desc":"switch one ark biz",
+            "id":"switchBiz"
+        },
+        {
+            "desc":"install one ark biz",
+            "id":"installBiz"
+        }
+    ]
+}
+

安装一个 biz

+
    +
  • URL: 127.0.0.1:1238/installBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName": "test",
+    "bizVersion": "1.0.0",
+    // local path should start with file://, alse support remote url which can be downloaded
+    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
+}
+
    +
  • 输出样例(成功):
  • +
+
{
+  "code":"SUCCESS",
+  "data":{
+    "bizInfos":[
+      {
+        "bizName":"dynamic-provider",
+        "bizState":"ACTIVATED",
+        "bizVersion":"1.0.0",
+        "declaredMode":true,
+        "identity":"dynamic-provider:1.0.0",
+        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+        "priority":100,
+        "webContextPath":"provider"
+      }
+    ],
+    "code":"SUCCESS",
+    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
+  }
+}
+

-输出样例(失败):

+
{
+  "code":"FAILED",
+  "data":{
+    "code":"REPEAT_BIZ",
+    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
+  }
+}
+

卸载模块

+
    +
  • URL: 127.0.0.1:1238/uninstallBiz
  • +
  • 输入样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+

-输出样例(成功):

+
{
+  "code":"SUCCESS"
+}
+
    +
  • 输出样例(失败):
  • +
+
{
+  "code":"FAILED",
+  "data":{
+    "code":"NOT_FOUND_BIZ",
+    "message":"Uninstall biz: test:1.0.0 not found."
+  }
+}
+

Switch a biz

+
    +
  • URL: 127.0.0.1:1238/switchBiz
  • +
  • 输出样例:
  • +
+
{
+    "bizName":"dynamic-provider",
+    "bizVersion":"1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS"
+}
+

查询所有 Biz

+
    +
  • URL: 127.0.0.1:1238/queryAllBiz
  • +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code":"SUCCESS",
+  "data":[
+    {
+      "bizName":"dynamic-provider",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
+      "webContextPath":"provider"
+    },
+    {
+      "bizName":"stock-mng",
+      "bizState":"ACTIVATED",
+      "bizVersion":"1.0.0",
+      "mainClass":"embed main",
+      "webContextPath":"/"
+    }
+  ]
+}
+

查询健康状况

+
    +
  • URL: 127.0.0.1:1238/health
  • +
+

以下根据不同的输入参数,获取到不同的状态信息

+

查询健康状况

+
    +
  • 输入样例:
  • +
+
{}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {
+        "max non heap memory(M)": -9.5367431640625E-7,
+        "java version": "1.8.0_331",
+        "max memory(M)": 885.5,
+        "max heap memory(M)": 885.5,
+        "used heap memory(M)": 137.14127349853516,
+        "used non heap memory(M)": 62.54662322998047,
+        "loaded class count": 10063,
+        "init non heap memory(M)": 2.4375,
+        "total memory(M)": 174.5,
+        "free memory(M)": 37.358726501464844,
+        "unload class count": 0,
+        "total class count": 10063,
+        "committed heap memory(M)": 174.5,
+        "java home": "****\\jre",
+        "init heap memory(M)": 64.0,
+        "committed non heap memory(M)": 66.203125,
+        "run time(s)": 34.432
+      },
+      "cpu": {
+        "count": 4,
+        "total used (%)": 131749.0,
+        "type": "****",
+        "user used (%)": 9.926451054656962,
+        "free (%)": 81.46475495070172,
+        "system used (%)": 6.249762806548817
+      },
+      "masterBizInfo": {
+        "webContextPath": "/",
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0"
+      },
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/2.2.3-SNAPSHOT/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        },
+        {
+          "artifactId": "runtime-sofa-boot-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.runtime.ark.plugin.SofaRuntimeActivator",
+          "pluginName": "runtime-sofa-boot-plugin",
+          "pluginUrl": "file:/****/runtime-sofa-boot-plugin-3.11.0.jar!/",
+          "pluginVersion": "3.11.0"
+        }
+      ],
+      "masterBizHealth": {
+        "readinessState": "ACCEPTING_TRAFFIC"
+      },
+      "bizListInfo": [
+        {
+          "bizName": "bookstore-manager",
+          "bizState": "ACTIVATED",
+          "bizVersion": "1.0.0",
+          "webContextPath": "/"
+        }
+      ]
+    }
+  }
+}
+

查询系统健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "system",
+  // [OPTIONAL] if metrics is null -> query all system health info
+  "metrics": ["cpu", "jvm"]
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "jvm": {...},
+      "cpu": {...},
+//      "masterBizHealth": {...}
+    }
+  }
+}
+

查询模块健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "biz",
+  // [OPTIONAL] if moduleName is null and moduleVersion is null -> query all biz
+  "moduleName": "bookstore-manager",
+  // [OPTIONAL] if moduleVersion is null -> query all biz named moduleName
+  "moduleVersion": "1.0.0"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "bizInfo": {
+        "bizName": "bookstore-manager",
+        "bizState": "ACTIVATED",
+        "bizVersion": "1.0.0",
+        "webContextPath": "/"
+      }
+//      "bizListInfo": [
+//        {
+//          "bizName": "bookstore-manager",
+//          "bizState": "ACTIVATED",
+//          "bizVersion": "1.0.0",
+//          "webContextPath": "/"
+//        }
+//      ]
+    }
+  }
+}
+

查询插件健康信息

+
    +
  • 输入样例:
  • +
+
{
+  "type": "plugin",
+  // [OPTIONAL] if moduleName is null -> query all biz
+  "moduleName": "web-ark-plugin"
+}
+
    +
  • 输出样例:
  • +
+
{
+  "code": "SUCCESS",
+  "data": {
+    "healthData": {
+      "pluginListInfo": [
+        {
+          "artifactId": "web-ark-plugin",
+          "groupId": "com.alipay.sofa",
+          "pluginActivator": "com.alipay.sofa.ark.web.embed.WebPluginActivator",
+          "pluginName": "web-ark-plugin",
+          "pluginUrl": "file:/****/web-ark-plugin-2.2.3-20230901.090402-2.jar!/",
+          "pluginVersion": "2.2.3-SNAPSHOT"
+        }
+      ]
+    }
+  }
+}
+

使用端点查询健康状况

+

使用端点获取 k8s 模块的健康信息

+

默认配置

+
    +
  • 端点暴露包括:*
  • +
  • 端点基本路径:/
  • +
  • 端点服务器端口:8080
  • +
+

http 代码结果

+
    +
  • HEALTHY(200):如果所有健康指标都是健康的,获取健康信息
  • +
  • UNHEALTHY(400):一旦健康指标不健康,获取健康信息
  • +
  • ENDPOINT_NOT_FOUND(404):找不到端点路径或参数
  • +
  • ENDPOINT_PROCESS_INTERNAL_ERROR(500):获取健康过程中抛出错误
  • +
+

查询所有健康信息

+
curl 127.0.0.1:8080/arkletHealth
+
    +
  • 输出样例
  • +
+
{   
+    "healthy": true,
+    "code": 200,    
+    "codeType": "HEALTHY",    
+    "data": {        
+        "jvm": {...},        
+        "masterBizHealth": {...},        
+        "cpu": {...},        
+        "masterBizInfo": {...},        
+        "bizListInfo": [...],        
+        "pluginListInfo": [...]    
+    }
+}  
+

查询所有 biz/plugin 健康信息

+
curl: 127.0.0.1:8080/arkletHealth/{moduleType} (moduleType 必须在 ['biz', 'plugin'])
+
    +
  • 输出样例
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizListInfo": [...],  
+       // "pluginListInfo": [...]      
+   }
+}  
+

查询单个 biz/plugin 健康信息

+
curl 127.0.0.1:8080/arkletHealth/{moduleType}/moduleName/moduleVersion (moduleType must in ['biz', 'plugin'])
+
    +
  • 输出样例:
  • +
+
{   
+   "healthy": true,
+   "code": 200,    
+   "codeType": "HEALTHY",    
+   "data": {        
+       "bizInfo": {...},  
+       // "pluginInfo": {...}      
+   }
+}
+

+ + +
+ + + + + + +
+ + +
+
+ 最后修改 December 8, 2023: add intro for arklet (733a2cdc) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html b/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html index 14deeadac..aed0fa600 100644 --- a/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html @@ -1,4 +1,25 @@ -如何发布 Arklet 版本 | SOFAServerless + + + + + + + + + + + + + + + + + + +如何发布 Arklet 版本 | SOFAServerless + + + + + + + + + + + + + + - - +仓库包同步与搜索 在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。"/> + + + + + + + + + + + + + + + + + + + -

如何发布 Arklet 版本

触发 github Action 发布到 snapshot staging

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
image.png

该 action 需要手动触发执行
image.png
执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

发布到 Release staging

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
点击左侧的 Staging Repositories:

搜索刚才记录的 ID:

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

仓库包同步与搜索

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。


-

最后修改 October 30, 2023: update home (f751b32b)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

如何发布 Arklet 版本

+ + +

触发 github Action 发布到 snapshot staging

+

版本发布到 maven 中央仓库,发布能力集成到了 github action 里:
+image.png

+

该 action 需要手动触发执行
+image.png
+执行成功后,只会发布到 snapshot staging,如果是 SNAPSHOT 版本,则这里执行完就可以结束。如果是正式版本,发布到 snapshot staging 之后,还需要推送到 release staging。

+

发布到 Release staging

+

打开  https://oss.sonatype.org ,点击右上角的 Log In, 登陆信息可找管理员。
+点击左侧的 Staging Repositories:
+

+

搜索刚才记录的 ID:
+

+

钩上之后就可以进行 Release (发布) 或者 Drop (放弃) 的操作。

+

当看不到这两个选项,只有 Close 选项时,则先选择 Close 操作,这时候如果包没有问题,则接下来可以 Release 或者 Drop。如果有问题,下面的内容中的 Activity 中会显示包不能正常 Close 的原因, 按照提示进行修改就可以了。

+

仓库包同步与搜索

+

在包发布到 release 仓库之后, 10 分钟后包会更新,在 http://central.maven.org/maven2/com/alipay/sofa/ 能看到包。2 小时之后,可通过 搜索 查询到包。

+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 30, 2023: update home (f751b32b) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/arklet/index.html b/docs/public/docs/contribution-guidelines/arklet/index.html index c7d0c1f35..442e1ce5c 100644 --- a/docs/public/docs/contribution-guidelines/arklet/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/index.html @@ -1,12 +1,502 @@ -Arklet 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Arklet 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Arklet 技术文档

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

Arklet 技术文档

+ + + +
+ + + + + + + + +
+ + +
+
+ Arklet 架构设计与接口设计 +
+

+
+ + +
+
+ 如何发布 Arklet 版本 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/communication-channel/index.html b/docs/public/docs/contribution-guidelines/communication-channel/index.html index c3815030f..04d21a85a 100644 --- a/docs/public/docs/contribution-guidelines/communication-channel/index.html +++ b/docs/public/docs/contribution-guidelines/communication-channel/index.html @@ -1,36 +1,533 @@ -交流渠道 | SOFAServerless + + + + + + + + + + + + + + + + + + +交流渠道 | SOFAServerless + + + + + + + + + + + + + + - - +每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。"/> + + + + + + + + + + + + + + + + + + + -

交流渠道

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

SOFAServerless 社区交流与协作钉钉群:24970018417

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。

SOFAServerless 用户微信群


如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。

社区双周会

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
具体会议时间也可关注社区钉钉协作群(群号:24970018417)


每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。



-

最后修改 November 14, 2023: update docs (bdf1ad9e)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

交流渠道

+ + +

SOFAServerless 提供如下沟通交流渠道,欢迎加入我们一起分享、一起使用、一起收获:

+

SOFAServerless 社区交流与协作钉钉群:24970018417

+

如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户、或者有兴趣成为社区 Contributor,都可以加入该钉钉群和我们随时随地一起交流讨论、一起贡献代码。
+

+

SOFAServerless 用户微信群

+ +
+如果您对 SOFAServerless 感兴趣、或者有初步意向使用 SOFAServerless、或者已经是 SOFAServerless / SOFAArk 的用户,都可以加入该微信群随时随地一起交流讨论。
+
+

社区双周会

+

每两周周二晚 19:30 - 20:30 会举办社区会议,下次社区双周会时间:2023 年 11 月 28 日 19:30 ~ 20:30,欢迎大家积极参与旁听或讨论。社区钉钉会议入会方式:
+入会链接:https://meeting.dingtalk.com/j/blp36k9mTbc
+钉钉会议号:90957500367
电话呼入:+867936169179 (中国大陆)、+867388953916 (中国大陆)
+具体会议时间也可关注社区钉钉协作群(群号:24970018417)

+
+

每个月底的周一会召开社区各组件 PMC 成员迭代规划会议,讨论并敲定下一个月需求规划。下次 PMC 成员月会时间:2023 年 11 月 27 日 19:30 ~ 20:30。入会方式同上。

+
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 November 14, 2023: update docs (bdf1ad9e) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/contribution/_print/index.html b/docs/public/docs/contribution-guidelines/contribution/_print/index.html index 5c66735f6..a50044d90 100644 --- a/docs/public/docs/contribution-guidelines/contribution/_print/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/_print/index.html @@ -1,56 +1,469 @@ -贡献社区 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +贡献社区 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - 本地开发测试

SOFAArk 和 Arklet

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

ModuleController

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
编译构建请在 module-controller 目录下执行:

go mod download   # if compile module-controller first time
-go build -a -o manager cmd/main.go  
-

单元测试执行请在 module-controller 目录下执行:

make test
-

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

Arkctl

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


2 - 完成第一次 PR 提交

认领或提交 Issue

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

获取源码

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

拉分支

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

  • 下载代码到本地,这一步您可以选择 git/https 方式:
git clone https://github.com/您的账号名/sofa-serverless.git
-
git clone https://github.com/您的账号名/sofa-ark.git
-
  • 拉分支准备修改代码:
git branch add_xxx_feature
-


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

  git branch -a
-

如果您想切换回主干,执行下面命令:

  git checkout -b master
-

如果您想切换回分支,执行下面命令:

  git checkout -b "branchName"
-

修改代码提交到本地

拉完分支后,就可以修改代码了。

修改代码注意事项

  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
mvn clean compile
-

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

  • 补充单元测试代码。
  • 确保新修改通过所有单元测试。
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
mvn clean test
-

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

make test
-

也可以通过 IDE 来辅助运行。

其它注意事项

  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • 对逻辑和功能不容易被理解的地方添加注释。
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。

修改完代码后,执行如下命令提交所有修改到本地:

git commit -am '添加xx功能'
-

提交代码到远程仓库

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

git push origin "branchname"
-

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

提交合并代码到主干的请求

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

PR 流水线校验

PR 流水线校验包括:

  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  3. 执行全部单元测试且必须全部通过。
  4. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  5. 检测提交的代码是否存在安全漏洞。
  6. 检测提交的代码是否符合基本代码规范。

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

Code Review

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

合并代码到主干

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。


3 - 文档、Issue、流程贡献

文档贡献

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 提交与回复贡献

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 模板

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

Question or Bug Report

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

### Describe the question or bug
-
-A clear and concise description of what the question or bug is.
-
-### Expected behavior
-
-A clear and concise description of what you expected to happen.
-
-### Actual behavior
-
-A clear and concise description of what actually happened.
-
-### Steps to reproduce
-
-Steps to reproduce the problem:
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-### Screenshots
-
-If applicable, add screenshots to help explain your problem.
-
-### Minimal yet complete reproducer code (or GitHub URL to code)
-
-### Environment
-
-- SOFAArk version:
-- JVM version (e.g. `java -version`):
-- OS version (e.g. `uname -a`):
-- Maven version:
-- IDE version:
-

Feature Request

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

流程贡献

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。



4 - 组织会议和运营布道

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. 与其他企业分享交流 SOFAServerless 的使用场景等。
  3. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  4. 其它运营方式。

- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

贡献社区

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - 本地开发测试

+ +

SOFAArk 和 Arklet

+

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

+

ModuleController

+

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
+编译构建请在 module-controller 目录下执行:

+
go mod download   # if compile module-controller first time
+go build -a -o manager cmd/main.go  
+

单元测试执行请在 module-controller 目录下执行:

+
make test
+

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
+module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

+

Arkctl

+

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ +
+ + + + + + + + + + + +
+ +

2 - 完成第一次 PR 提交

+ +

认领或提交 Issue

+

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

+

获取源码

+

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

+

拉分支

+

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

+
    +
  • 下载代码到本地,这一步您可以选择 git/https 方式:
  • +
+
git clone https://github.com/您的账号名/sofa-serverless.git
+
git clone https://github.com/您的账号名/sofa-ark.git
+
    +
  • 拉分支准备修改代码:
  • +
+
git branch add_xxx_feature
+


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

+
  git branch -a
+

如果您想切换回主干,执行下面命令:

+
  git checkout -b master
+

如果您想切换回分支,执行下面命令:

+
  git checkout -b "branchName"
+

修改代码提交到本地

+

拉完分支后,就可以修改代码了。

+

修改代码注意事项

+
    +
  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
  • +
+
mvn clean compile
+

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

+
    +
  • 补充单元测试代码。
  • +
  • 确保新修改通过所有单元测试。
  • +
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
  • +
+
mvn clean test
+

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

+
make test
+

也可以通过 IDE 来辅助运行。

+

其它注意事项

+
    +
  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • +
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • +
  • 对逻辑和功能不容易被理解的地方添加注释。
  • +
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。
  • +
+

修改完代码后,执行如下命令提交所有修改到本地:

+
git commit -am '添加xx功能'
+

提交代码到远程仓库

+

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

+
git push origin "branchname"
+

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

+

提交合并代码到主干的请求

+

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

+

PR 流水线校验

+

PR 流水线校验包括:

+
    +
  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. +
  3. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  4. +
  5. 执行全部单元测试且必须全部通过。
  6. +
  7. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  8. +
  9. 检测提交的代码是否存在安全漏洞。
  10. +
  11. 检测提交的代码是否符合基本代码规范。
  12. +
+

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

+

Code Review

+

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

+

合并代码到主干

+

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。

+
+ +
+ + + + + + + + + + + +
+ +

3 - 文档、Issue、流程贡献

+ +

文档贡献

+

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 提交与回复贡献

+

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 模板

+

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

+

Question or Bug Report

+

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

+
### Describe the question or bug
+
+A clear and concise description of what the question or bug is.
+
+### Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+### Actual behavior
+
+A clear and concise description of what actually happened.
+
+### Steps to reproduce
+
+Steps to reproduce the problem:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+### Screenshots
+
+If applicable, add screenshots to help explain your problem.
+
+### Minimal yet complete reproducer code (or GitHub URL to code)
+
+### Environment
+
+- SOFAArk version:
+- JVM version (e.g. `java -version`):
+- OS version (e.g. `uname -a`):
+- Maven version:
+- IDE version:
+

Feature Request

+

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

+

流程贡献

+

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。

+
+
+ +
+ + + + + + + + + + + +
+ +

4 - 组织会议和运营布道

+ +

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

+
    +
  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. +
  3. 与其他企业分享交流 SOFAServerless 的使用场景等。
  4. +
  5. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  6. +
  7. 其它运营方式。
  8. +
+
+ +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html b/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html index 3a5eac5e8..a78f82b87 100644 --- a/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html @@ -1,47 +1,596 @@ -完成第一次 PR 提交 | SOFAServerless + + + + + + + + + + + + + + + + + + +完成第一次 PR 提交 | SOFAServerless + + + + + + + + + + + + + + - - +git checkout -b "branchName" 修改代码提交到本地 拉完分支后,就可以修改代码了。"/> + + + + + + + + + + + + + + + + + + + -

完成第一次 PR 提交

认领或提交 Issue

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

获取源码

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

拉分支

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

  • 下载代码到本地,这一步您可以选择 git/https 方式:
git clone https://github.com/您的账号名/sofa-serverless.git
-
git clone https://github.com/您的账号名/sofa-ark.git
-
  • 拉分支准备修改代码:
git branch add_xxx_feature
-


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

  git branch -a
-

如果您想切换回主干,执行下面命令:

  git checkout -b master
-

如果您想切换回分支,执行下面命令:

  git checkout -b "branchName"
-

修改代码提交到本地

拉完分支后,就可以修改代码了。

修改代码注意事项

  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
mvn clean compile
-

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

  • 补充单元测试代码。
  • 确保新修改通过所有单元测试。
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
mvn clean test
-

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

make test
-

也可以通过 IDE 来辅助运行。

其它注意事项

  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • 对逻辑和功能不容易被理解的地方添加注释。
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。

修改完代码后,执行如下命令提交所有修改到本地:

git commit -am '添加xx功能'
-

提交代码到远程仓库

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

git push origin "branchname"
-

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

提交合并代码到主干的请求

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

PR 流水线校验

PR 流水线校验包括:

  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  3. 执行全部单元测试且必须全部通过。
  4. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  5. 检测提交的代码是否存在安全漏洞。
  6. 检测提交的代码是否符合基本代码规范。

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

Code Review

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

合并代码到主干

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。


-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

完成第一次 PR 提交

+ + +

认领或提交 Issue

+

不论您是修复 bug、新增功能或者改进现有功能,在您提交代码之前,请在 SOFAServerlessSOFAArk GitHub 上认领一个 Issue 并将 Assignee 指定为自己(新人建议认领 good-first-issue 标签的新手任务)。或者提交一个新的 Issue,描述您要修复的问题或者要增加、改进的功能。这样做的好处是能避免与其他人的工作重复

+

获取源码

+

要修改或新增功能,在提 Issue 或者领取现有 Issue 后,点击左上角的fork按钮,复制一份 SOFAServerless 或 SOFAArk 主干代码到您的代码仓库。

+

拉分支

+

SOFAServerless 和 SOFAArk 所有修改都在个人分支上进行,修改完后提交 pull request,当前在跑通 PR 流水线之后,会由相应组件的 PMC 或 Maintainer 负责 Review 与合并代码到主干(master)。因此,在 fork 源码后,您需要:

+
    +
  • 下载代码到本地,这一步您可以选择 git/https 方式:
  • +
+
git clone https://github.com/您的账号名/sofa-serverless.git
+
git clone https://github.com/您的账号名/sofa-ark.git
+
    +
  • 拉分支准备修改代码:
  • +
+
git branch add_xxx_feature
+


执行完上述命令后,您的代码仓库就切换到相应分支了。执行如下命令可以看到您当前分支:

+
  git branch -a
+

如果您想切换回主干,执行下面命令:

+
  git checkout -b master
+

如果您想切换回分支,执行下面命令:

+
  git checkout -b "branchName"
+

修改代码提交到本地

+

拉完分支后,就可以修改代码了。

+

修改代码注意事项

+
    +
  • 代码风格保持一致。SOFAServerless arklet 和 sofa-ark 通过 Maven 插件来保持代码格式一致,在提交代码前,务必先本地执行:
  • +
+
mvn clean compile
+

module-controller 和 arkctl Golang 代码的格式化能力还在建设中。

+
    +
  • 补充单元测试代码。
  • +
  • 确保新修改通过所有单元测试。
  • +
  • 如果是 bug 修复,应该提供新的单元测试来证明以前的代码存在 bug,而新的代码已经解决了这些 bug。对于 arklet 和 sofa-ark 您可以用如下命令运行所有测试:
  • +
+
mvn clean test
+

对于 module-controller 和 arkctl,您可以用如下命令运行所有测试:

+
make test
+

也可以通过 IDE 来辅助运行。

+

其它注意事项

+
    +
  • 请保持您编辑的代码使用原有风格,尤其是空格换行等。
  • +
  • 对于无用的注释,请直接删除。注释必须使用英文。
  • +
  • 对逻辑和功能不容易被理解的地方添加注释。
  • +
  • 务必第一时间更新 docs/content/zh-cn/ 目录中的 “docs”、“contribution-guidelines” 目录中的相关文档。
  • +
+

修改完代码后,执行如下命令提交所有修改到本地:

+
git commit -am '添加xx功能'
+

提交代码到远程仓库

+

在代码提交到本地后,就是与远程仓库同步代码了。执行如下命令提交本地修改到 github 上:

+
git push origin "branchname"
+

如果前面您是通过 fork 来做的,那么这里的 origin 是 push 到您的代码仓库,而不是 SOFAServerless 的代码仓库。

+

提交合并代码到主干的请求

+

在的代码提交到 GitHub 后,您就可以发送请求来把您改好的代码合入 SOFAServerless 或 SOFAArk 主干代码了。此时您需要进入您的 GitHub 上的对应仓库,按右上角的 pull request按钮。选择目标分支,一般就是 master,当前需要选择组件的 MaintainerPMC 作为 Code Reviewer,如果 PR 流水线校验和 Code Review 都通过,您的代码就会合入主干成为 SOFAServerless 的一部分。

+

PR 流水线校验

+

PR 流水线校验包括:

+
    +
  1. CLA 签署。第一次提交 PR 必须完成 CLA 协议的签署,如果打不开 CLA 签署页面请尝试使用代理。
  2. +
  3. 自动为每个文件追加 Apache 2.0 License 声明和作者。
  4. +
  5. 执行全部单元测试且必须全部通过。
  6. +
  7. 检测覆盖率是否达到行覆盖 >= 80%,分支覆盖 >= 60%。
  8. +
  9. 检测提交的代码是否存在安全漏洞。
  10. +
  11. 检测提交的代码是否符合基本代码规范。
  12. +
+

以上校验必须全部通过,PR 流水线才会通过并进入到 Code Review 环节。

+

Code Review

+

当您选择对应组件的 MaintainerPMC 作为 Code Reviewer 数天后,仍然没有人对您的提交给予任何回复,可以在 PR 下面留言并 at 相关人员,或者在社区钉钉协作群中(钉钉群号:24970018417)直接 at 相关人员 Review 代码。对于 Code Review 的意见,Code Reviewer 会直接备注到到对应的 PR 或者 Issue 中,如果您觉得建议是合理的,也请您把这些建议更新到您的代码中并重新提交 PR。

+

合并代码到主干

+

在 PR 流水线校验和 Code Review 都通过后,就由 SOFAServerless 维护人员操作合入主干了,代码合并之后您会收到合并成功的提示。

+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html b/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html index afaf35aa3..dac16048e 100644 --- a/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html @@ -1,62 +1,559 @@ -文档、Issue、流程贡献 | SOFAServerless + + + + + + + + + + + + + + + + + + +文档、Issue、流程贡献 | SOFAServerless + + + + + + + + + + + + + + - - +### Describe the question or bug A clear and concise description of what the question or bug is."/> + + + + + + + + + + + + + + + + + + + -

文档、Issue、流程贡献

文档贡献

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 提交与回复贡献

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

Issue 模板

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

Question or Bug Report

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

### Describe the question or bug
-
-A clear and concise description of what the question or bug is.
-
-### Expected behavior
-
-A clear and concise description of what you expected to happen.
-
-### Actual behavior
-
-A clear and concise description of what actually happened.
-
-### Steps to reproduce
-
-Steps to reproduce the problem:
-
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-### Screenshots
-
-If applicable, add screenshots to help explain your problem.
-
-### Minimal yet complete reproducer code (or GitHub URL to code)
-
-### Environment
-
-- SOFAArk version:
-- JVM version (e.g. `java -version`):
-- OS version (e.g. `uname -a`):
-- Maven version:
-- IDE version:
-

Feature Request

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

流程贡献

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。



-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

文档、Issue、流程贡献

+ + +

文档贡献

+

使用文档、技术文档、官网内容需要社区每一位 Contributor 共同维护,对任意文档和官网内容做出贡献的同学都是我们的 Contributor,并且根据活跃度有机会成为 SOFAServerless 组件的 Committer 甚至 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 提交与回复贡献

+

任何使用过程中的问题、Bug、新功能、改进优化请创建 GitHub Issue,社区每天会有值班同学负责跟进 Issue。任何人提出或者回复 Issue 都是 SOFAServerless 的 Contributor,对回复 Issue 活跃的 Contributor 可以晋升为 Committer,如果特别活跃甚至可以晋升为 PMC 成员,共同主导 SOFAServerless 的技术演进。

+

Issue 模板

+

SOFAServerless(含 SOFAArk)Issue 有两种模板,一种是 “Question or Bug Report”,一种是 “Feature Request”。
image.png

+

Question or Bug Report

+

所有使用过程中遇到的问题或者疑似 Bug,请选择 “Question or Bug Report”,并提供详细的复现信息如下:

+
### Describe the question or bug
+
+A clear and concise description of what the question or bug is.
+
+### Expected behavior
+
+A clear and concise description of what you expected to happen.
+
+### Actual behavior
+
+A clear and concise description of what actually happened.
+
+### Steps to reproduce
+
+Steps to reproduce the problem:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+### Screenshots
+
+If applicable, add screenshots to help explain your problem.
+
+### Minimal yet complete reproducer code (or GitHub URL to code)
+
+### Environment
+
+- SOFAArk version:
+- JVM version (e.g. `java -version`):
+- OS version (e.g. `uname -a`):
+- Maven version:
+- IDE version:
+

Feature Request

+

新功能、已有功能改进优化或者其它讨论,请选择 “Feature Request”。

+

流程贡献

+

SOFAServerless 当前制定了代码规约、PR 流程、CI 流水线、迭代管理、周会、交流渠道等各种协作规范,您可以对我们的协作规范和流程在 GitHub 上提出建议,即可成为我们的 Contributor。

+
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/contribution/index.html b/docs/public/docs/contribution-guidelines/contribution/index.html index 9d8088158..a19872080 100644 --- a/docs/public/docs/contribution-guidelines/contribution/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/index.html @@ -1,12 +1,518 @@ -贡献社区 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +贡献社区 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

贡献社区

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

贡献社区

+ + + +
+ + + + + + + + +
+ + +
+
+ 本地开发测试 +
+

+
+ + +
+
+ 完成第一次 PR 提交 +
+

+
+ + +
+
+ 文档、Issue、流程贡献 +
+

+
+ + +
+
+ 组织会议和运营布道 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html b/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html index bc0235190..22ecea330 100644 --- a/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html @@ -1,43 +1,531 @@ -本地开发测试 | SOFAServerless + + + + + + + + + + + + + + + + + + +本地开发测试 | SOFAServerless + + + + + + + + + + + + + + - - +Arkctl Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中。"/> + + + + + + + + + + + + + + + + + + + -

本地开发测试

SOFAArk 和 Arklet

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

ModuleController

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
编译构建请在 module-controller 目录下执行:

go mod download   # if compile module-controller first time
-go build -a -o manager cmd/main.go  
-

单元测试执行请在 module-controller 目录下执行:

make test
-

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

Arkctl

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中


-

最后修改 November 3, 2023: Revert "update docs" (d7d8e5c4)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

本地开发测试

+ + +

SOFAArk 和 Arklet

+

SOFAArk 是一个普通 Java SDK 项目,使用 Maven 作为依赖管理和构建工具,只需要本地安装 Maven 3.6 及以上版本即可正常开发代码和单元测试,无需其它的环境准备工作。
关于代码提交细节请参考:完成第一次 PR 提交

+

ModuleController

+

ModuleController 是一个标准的 K8S Golang Operator 组件,里面包含了 ModuleDeployment Operator、ModuleReplicaSet Operator、Module Operator,在本地可以使用 minikube 做开发测试,具体请参考本地快速开始
+编译构建请在 module-controller 目录下执行:

+
go mod download   # if compile module-controller first time
+go build -a -o manager cmd/main.go  
+

单元测试执行请在 module-controller 目录下执行:

+
make test
+

您也可以使用 IDE 进行编译构建、开发调试和单元测试执行。
+module-controller 开发方式和标准 K8S Operator 开发方式完全一样,您可以参考 K8S Operator 开发官方文档

+

Arkctl

+

Arkctl 是一个普通 Golang 项目,他是一个命令行工具集,包含了用户在本地开发和运维模块过程中的常用工具,它和普通 Golang 程序开发完全一样,当前初始版本还在开发中

+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 November 3, 2023: Revert "update docs" (d7d8e5c4) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/contribution/sermon/index.html b/docs/public/docs/contribution-guidelines/contribution/sermon/index.html index 72729dd1e..c724a65ee 100644 --- a/docs/public/docs/contribution-guidelines/contribution/sermon/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/sermon/index.html @@ -1,16 +1,492 @@ -组织会议和运营布道 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +组织会议和运营布道 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

组织会议和运营布道

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. 与其他企业分享交流 SOFAServerless 的使用场景等。
  3. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  4. 其它运营方式。

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

组织会议和运营布道

+ + +

我们鼓励大家宣传、布道 SOFAServerless,通过运营成为 SOFAServerless 的 Contributor、Committer 甚至 PMC,每一次 Contributor 的晋升,我们也会发放纪念品奖励。运营方式包括但不限于:

+
    +
  1. 在线上或线下技术会议、Meetup 中发表 SOFAServerless 的使用或者技术实现相关演讲。
  2. +
  3. 与其他企业分享交流 SOFAServerless 的使用场景等。
  4. +
  5. 在各种渠道发表关于 SOFAServerless 的使用或者技术实现相关文章或视频。
  6. +
  7. 其它运营方式。
  8. +
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/index.html b/docs/public/docs/contribution-guidelines/index.html index f54c0c804..370677ab9 100644 --- a/docs/public/docs/contribution-guidelines/index.html +++ b/docs/public/docs/contribution-guidelines/index.html @@ -1,12 +1,558 @@ -参与社区 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +参与社区 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

参与社区

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

参与社区

+ + + +
+ + + + + + + + +
+ + +
+
+ 开放包容理念 +
+

+
+ + +
+
+ 交流渠道 +
+

+
+ + +
+
+ 贡献社区 +
+

+
+ + +
+
+ 社区角色与晋升 +
+

+
+ + +
+
+ SOFAArk 技术文档 +
+

+
+ + +
+
+ Arklet 技术文档 +
+

+
+ + +
+
+ ModuleController 技术文档 +
+

+
+ + +
+
+ Arkctl 技术文档 +
+

+
+ + +
+
+ 多模块运行时适配或最佳实践 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/module-controller/_print/index.html b/docs/public/docs/contribution-guidelines/module-controller/_print/index.html index 3a80beb6e..f85d99f84 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/_print/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/_print/index.html @@ -1,8 +1,439 @@ -ModuleController 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +ModuleController 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - ModuleController 架构设计

介绍

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

基本架构

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。

功能清单和 RoadMap

  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)

2 - CRD 模型设计

CRD 模型对比

K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。

ModuleDeployment CRD 模型

image

Module CRD 模型

image

ModuleTemplate CRD 模型

image

ModuleReplicaSet CRD 模型

image


3 - 核心代码结构

image.png

image.png


核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。



4 - 模块生命周期

模块生命周期

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

image

模块状态机

image


5 - 核心流程时序图

模块首发

image

模块二发

image

模块下线

image

对等基座扩容

image

对等基座缩容

image

- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

ModuleController 技术文档

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - ModuleController 架构设计

+ +

介绍

+

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

+

基本架构

+

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。
+

+

功能清单和 RoadMap

+
    +
  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • +
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • +
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • +
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • +
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • +
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)
  • +
+
+ +
+ + + + + + + + + + + +
+ +

2 - CRD 模型设计

+ +

CRD 模型对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。
+

ModuleDeployment CRD 模型

+

image

+

Module CRD 模型

+

image

+

ModuleTemplate CRD 模型

+

image

+

ModuleReplicaSet CRD 模型

+

image

+
+ +
+ + + + + + + + + + + +
+ +

3 - 核心代码结构

+ +

image.png

+

image.png

+
+

核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。

+
+
+ +
+ + + + + + + + + + + +
+ +

4 - 模块生命周期

+ +

模块生命周期

+

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

+

image

+

模块状态机

+

image

+
+ +
+ + + + + + + + + + + +
+ +

5 - 核心流程时序图

+ +

模块首发

+

image

+

模块二发

+

image

+

模块下线

+

image

+

对等基座扩容

+

image

+

对等基座缩容

+

image +

+ +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html b/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html index e40e4a240..0bd87687b 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html @@ -1,20 +1,512 @@ -ModuleController 架构设计 | SOFAServerless + + + + + + + + + + + + + + + + + + +ModuleController 架构设计 | SOFAServerless + + + + + + + + + + + + + + - - +功能清单和 RoadMap 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动) 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南) 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试) 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试) 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试) 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善) "/> + + + + + + + + + + + + + + + + + + + -

ModuleController 架构设计

介绍

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

基本架构

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。

功能清单和 RoadMap

  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)

-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

ModuleController 架构设计

+ + +

介绍

+

ModuleController 是一个 K8S 控制器,该控制器参考 K8S 架构,定义并且实现了 ModuleDeployment、ModuleReplicaSet、Module 等核心模型与调和能力,从而实现了 Serverless 模块的秒级运维调度,以及与基座的联动运维能力。

+

基本架构

+

ModuleController 目前包含 ModuleDeployment Opertor、ModuleReplicaSet Operator、Module Operator 三个组件。和 K8S 原生 Deployment 类似,用户创建 ModuleDeployment 会调和出 ModuleReplicaSet,ModuleReplicaSet 会进一步调和出 Module,最终 Module Operator 会调用 Pod 里的 Arklet SDK 去安装或卸载模块。此外 ModuleController 还会为 ModuleDeployment 自动生成 K8S Service,企业可以监听该 Service 的 IP 变化实现与自身流量控制系统的集成,从而实现模块粒度的切流和挂流。
+

+

功能清单和 RoadMap

+
    +
  • 08.15:0.2 版本上线(包括非对等模块发布、卸载、扩缩容、副本保持、基座运维联动)
  • +
  • 08.25:0.3 版本上线(包括回滚链路、各项参数校验、单测达到 80/60、CI 自动化、开发者指南)
  • +
  • 09.31:0.5 版本上线(1:1 先扩后缩、模块回滚、两种调度策略、状态回流、1+ 端到端集成测试)
  • +
  • 10.30:0.6 版本上线(支持以 K8S Service 方式联动企业四七层流量控制、总计 10+ 端到端集成测试)
  • +
  • 11.30:1.0 版本上线(支持对等发布运维、各项修复打磨、总计 20+ 端到端集成测试)
  • +
  • 12.30:1.1 版本上线(支持模块和基座自动弹性伸缩、对等与非对等发布运维能力完善)
  • +
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html b/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html index 6d5f47c2d..e0b8e052e 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html @@ -1,12 +1,486 @@ -核心代码结构 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +核心代码结构 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

核心代码结构

image.png

image.png


核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。



-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

核心代码结构

+ + +

image.png

+

image.png

+
+

核心代码逻辑在 moduledeployment_controller.go、modulereplicaset_controller.go、module_controller.go、controller_utils.go,里面有详细注释。

+
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html b/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html index 12a89715c..ca95eb1be 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html @@ -1,12 +1,537 @@ -CRD 模型设计 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +CRD 模型设计 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

CRD 模型设计

CRD 模型对比

K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。

ModuleDeployment CRD 模型

image

Module CRD 模型

image

ModuleTemplate CRD 模型

image

ModuleReplicaSet CRD 模型

image


-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

CRD 模型设计

+ + +

CRD 模型对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
K8S 原生 CRDModuleController CRD关系和区别
PodModulePod:K8S 中创建和管理的、最小的可部署的计算单元。     Module:Serverless 创建和管理的、最小的可部署的计算单元。
PodSpecModuleSpecPodSpec:对 Pod 的描述。包含容器、调度、卷等。     ModuleSpec:对 Module 的描述,包含模块、服务、调度(亲和性)。
PodTemplateModuleTemplatePodTemplate:定义 Pod 的生成副本,包含 PodSpec。     ModuleTemplate:定义 Module 的生成副本,包含 ModuleGroupSpec。
DeploymentModuleDeploymentDeployment:定义 Pod 的期望状态和副本数量。     ModuleDeployment:定义 Module 的期望状态和副本数量。
ReplicaSetModuleReplicaSetReplicaSet:管理 Pod 的运行副本。    
ModuleReplicaSet:管理 Module 的运行副本。
+

ModuleDeployment CRD 模型

+

image

+

Module CRD 模型

+

image

+

ModuleTemplate CRD 模型

+

image

+

ModuleReplicaSet CRD 模型

+

image

+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/module-controller/index.html b/docs/public/docs/contribution-guidelines/module-controller/index.html index bdb0e36f3..4d61b6bb8 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/index.html @@ -1,12 +1,526 @@ -ModuleController 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +ModuleController 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

ModuleController 技术文档

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

ModuleController 技术文档

+ + + +
+ + + + + + + + +
+ + +
+
+ ModuleController 架构设计 +
+

+
+ + +
+
+ CRD 模型设计 +
+

+
+ + +
+
+ 核心代码结构 +
+

+
+ + +
+
+ 模块生命周期 +
+

+
+ + +
+
+ 核心流程时序图 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html b/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html index 8652482f7..12b86233c 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html @@ -1,16 +1,502 @@ -模块生命周期 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +模块生命周期 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

模块生命周期

模块生命周期

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

image

模块状态机

image


-

- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

模块生命周期

+ + +

模块生命周期

+

4象限描述了模块的生命周期: Prepare、Upgrading、Completed、Available

+

image

+

模块状态机

+

image

+
+ + +
+ + + + + + +
+ + +
+ + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html b/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html index dc452b12c..eb7988ccc 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html @@ -1,12 +1,502 @@ -核心流程时序图 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +核心流程时序图 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

核心流程时序图

模块首发

image

模块二发

image

模块下线

image

对等基座扩容

image

对等基座缩容

image

-

- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

核心流程时序图

+ + +

模块首发

+

image

+

模块二发

+

image

+

模块下线

+

image

+

对等基座扩容

+

image

+

对等基座缩容

+

image +

+ + +
+ + + + + + +
+ + +
+ + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/our-vision/index.html b/docs/public/docs/contribution-guidelines/our-vision/index.html index b386c4988..9120039b4 100644 --- a/docs/public/docs/contribution-guidelines/our-vision/index.html +++ b/docs/public/docs/contribution-guidelines/our-vision/index.html @@ -1,40 +1,567 @@ -开放包容理念 | SOFAServerless + + + + + + + + + + + + + + + + + + +开放包容理念 | SOFAServerless + + + + + + + + + + + + + + - - +KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。"/> + + + + + + + + + + + + + + + + + + + -

开放包容理念

核心价值观

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

  1. 社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

  2. 社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

  3. 社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

2023 年 OKR

O1 打造社区健康、有行业影响力的 Serverless 开源产品

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

O2 打造技术先进、效果显著的降本增效解决方案

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

RoadMap

  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • 2023.09 官网和完整用户手册上线。
  • 2023.10 新增 2+ 公司使用。
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。


-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

开放包容理念

+ + +

核心价值观

+

SOFAServerless 社区的核心价值观是 “开放” 和 “包容”。社区里所有的用户、开发者完全平等,体现在如下几个方面:

+
    +
  1. +

    社区参考了 Apache 开源项目的运作方式,对社区做出任意贡献的同学,尤其是非代码贡献的同学(文档、官网、Issue 回复、运营布道、发展建议等),都是我们的 Contributor,都有机会成为社区的 Committer 甚至是 PMC(Project Management Committee)成员。

    +
  2. +
  3. +

    社区所有的 OKR、RoadMap、讨论、会议、技术方案等都是完全开放的,所有人都可以看见,并且都可以参与其中,社区会认证倾听、考虑大家的所有建议和意见,一旦采纳就会确保执行落地。希望大家带着无所顾虑、求同尊异的心态参与 SOFAServerless 社区。

    +
  4. +
  5. +

    社区不限地域国籍,所有源代码必须是英文注释确保大家都能理解,官网也是中英文双语。所有微信群、钉钉群、GitHub Issues 讨论都可以是中英双语。但由于当前我们主要聚焦在国内用户,因此大部分文档暂时只有中文版,未来会提供英文版。

    +
  6. +
+

2023 年 OKR

+

O1 打造社区健康、有行业影响力的 Serverless 开源产品

+

KR1 新增 10 个 Contributors,年底 OpenRank 指数 > 15(当前 5)、活跃度 > 50(当前 44)

+

KR1.1 完成 5 次布道和 5 次文章分享,触达 200 家企业,深度交流 30+ 企业。
+KR1.2 形成完善的社区共建机制(包括 Issue 管理、文档、问题响应、培养与晋升机制),发布 2+ 培训课程与产品手册,共建开发者可在一周内上手,开发总吞吐率达到 20+ Issues/周。

+

KR2 新增 5 家企业在生产环境使用或完成试点接入(当前新增 1),3 家企业参与社区

+

KR2.1 产出初步行业分析报告,帮助定位适用不同场景的重点企业对象。
+KR2.2 5 家企业生产真实使用或完成试点接入,3 家企业参与社区,覆盖 3 个场景并沉淀 3+ 用户案例。

+

O2 打造技术先进、效果显著的降本增效解决方案

+

KR1 落地模块化技术实现机器减少 30%、部署验证耗时降低至 30 秒、需求交付效率提升 50%

+

KR1.1 搭建 1 分钟快速试用平台,完善的文档、官网与配套支持,用户可在 10 分钟完成一个模块拆分。
+KR1.2 完成 20 种中间件和三方包治理,同时形成多应用与热卸载评测和自动检测标准。
+KR1.3 模块具备热部署启动耗时降低至 10 秒级,多模块具备合并部署资源减少 30%,同时让用户需求交付效率提升 50%。
+KR1.4 落地开源版 Arklet,支持 SOFABoot 和 SpringBoot。提供运维管道、指标采集、模块生命周期管理、多模块运行环境、Bean 与服务发现及调用能力。
+KR1.5 落地研发工具 ArkCtl,具备快速开发验证、灵活部署(合并与独立部署)、模块低成本拆分改造能力。

+

KR2 运维调度 1.0 版本上线。全链路高频端到端测试用例成功率 99.9%,自身端到端耗时 P90 < 500ms

+

KR2.1 上线基于 K8S Operator 的开源版运维调度能力,至少具备发布、回滚、下线、扩缩容、替换、副本保持、2+ 调度策略、模块流控、部署策略、对等和非对等运维能力。
+KR2.2 建设开源版 CI 和 25+ 高频端到端测试用例,不断打磨并推动端到端 P90 耗时 < 500ms、所有预演成功率> 99.9%、单测覆盖率达到行 > 80% 分支 > 60%(通过率 100%)。

+

KR3 开源版自动伸缩初步上线,模块具备人工画像和分时伸缩能力

+

RoadMap

+
    +
  • 2023.08 完成 SOFABoot 完整的部署功能验证,产出兼容性 Benchmark 基线。
  • +
  • 2023.09 发布基础运维和调度系统 ModuleController 0.5 版本。
  • +
  • 2023.09 发布研发运维工具 Arkctl 与 Arklet 0.5 版本。
  • +
  • 2023.09 官网和完整用户手册上线。
  • +
  • 2023.10 新增 2+ 公司使用。
  • +
  • 2023.11 支持 SpringBoot 完整能力和 5+ 社区常用中间件。
  • +
  • 2023.11 SOFAServerless 1.0 版本上线(ModuleController、Arkctl、Arklet、SpringBoot 兼容)。
  • +
  • 2023.12 SOFAServerless 1.1 版本上线(包括基础自动伸缩、模块基础拆分工具、20+ 中间件与三方包兼容)。
  • +
  • 2023.12 新增 5+ 家公司真实使用,10+ Contributors 参与。
  • +
+
+
+ +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/role-and-promotion/index.html b/docs/public/docs/contribution-guidelines/role-and-promotion/index.html index 5c43ee88d..a2bfaae3c 100644 --- a/docs/public/docs/contribution-guidelines/role-and-promotion/index.html +++ b/docs/public/docs/contribution-guidelines/role-and-promotion/index.html @@ -1,24 +1,577 @@ -社区角色与晋升 | SOFAServerless + + + + + + + + + + + + + + + + + + +社区角色与晋升 | SOFAServerless + + + + + + + + + + + + + + - - +PMC (Project Management Comittee) glmapper"/> + + + + + + + + + + + + + + + + + + + -

社区角色与晋升

角色职责与晋升机制

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。

社区角色成员名单

SOFAArk

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Comittee)

glmapper

Committer

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

Contributor

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

Arklet

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

TomorJM

Committer

暂无

Contributor

glmapper
Lunarscave
lylingzhen

ModuleController

Maintainer

gold300jin

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

liu-657667
Charlie17Li
lylingzhen

Arkctl

Maintainer

yuanyuancin
lvjing2

PMC (Project Management Committee)

暂无

Committer

暂无

Contributor

暂无


-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

社区角色与晋升

+ + +

角色职责与晋升机制

+

SOFAServerless 社区角色参考了 Apache 开源产品组织方式,SOFAArk、Arklet、ModuleController、ArkCtl 每个组件都有各自的角色。每个组件的角色职责从低到高分别是:Contributor、Committer、PMC (Project Management Committee)、Maintainer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
角色责任与权限晋升到更高角色机制
Contributor所有在社区提 Issue、回答 Issue、对外运营、提交文档内容或者提交任意代码的同学,都是相应组件的 Contributor。Contributor 拥有 Issue 提交、Issue 回复、官网或文档内容提交、代码提交(不包括代码评审)和对外发表文章权限。当 Contributor 完成合并的代码或者文档内容足够多,就可以由该组件的 PMC 成员投票晋升为 Committer。当 Contributor 回答的 Issue 或者参与的运营活动足够多,也可以被 PMC 成员投票晋升为 Committer。
Committer所有在社区积极回答 Issue、对外运营、提交文档内容或者提交代码的同学,按积极度都有可能被 PMC 成员投票晋升为 Committer。Committer 额外拥有代码评审、技术方案评审、Contributor 培养的责任与权限。对长期积极投入或持续有突出贡献的 Committer,经 PMC 成员投票可以晋升为相应组件的 PMC 成员。
PMC对相应组件持续贡献特别活跃的同学有机会晋升为 PMC 成员。PMC 成员额外拥有组件的 RoadMap 制定、技术方案和代码评审、Issue 和迭代管理、Contributor 和 Committer 培养等责任与权限。
MaintainerMaintainer 额外拥有密钥管理和仓库管理等管理员权限,除此之外在其他方面和 PMC 成员的责任与权限是完全对等的。
+

社区角色成员名单

+

SOFAArk

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Comittee)

+

glmapper

+

Committer

+

zjulbj5
gaosaroma
QilongZhang133
straybirdzls13
caojie0911

+

Contributor

+

lylingzhen10
khotyn
FlyAbner (260+ 行提交,提名 Comitter?)
alaneuler
sususama
ujjboy
JoeKerouac
Lunarscave
HzjNeverStop
AiWu4Damon
vchangpengfei
HuangDayu
shenchao45
DalianRollingKing
nobodyiam
lanicc
azhsmesos
wuqian0808
KangZhiDong
suntao4019
huangyunbin
jiangyunpeng
michalyao
rootsongjc
Zwl0113
tofdragon
lishiguang4
hionwi
343585776
g-stream
zkitcast
davidzj
zyclove
WindSearcher
lovejin52022
smalljunHw
vchangpengfei
sq1015
xwh1108
yuanChina
blysin
yuwenkai666
hadoop835
gitYupan
thirdparty-core
Estom
jijuanwang
DCLe-DA
linkoog
springcoco
zhaowwwjian
xingcici
ixufeng
jnan806
lizhi12q
kongqq
wangxiaotao00
由于篇幅有限,23 年之前提交 Issue 的 Contributor 不在此一一列举,也同样感谢大家对 SOFAArk 的使用和咨询

+

Arklet

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Committee)

+

TomorJM

+

Committer

+

暂无

+

Contributor

+

glmapper
Lunarscave
lylingzhen

+

ModuleController

+

Maintainer

+

gold300jin

+

PMC (Project Management Committee)

+

暂无

+

Committer

+

暂无

+

Contributor

+

liu-657667
Charlie17Li
lylingzhen

+

Arkctl

+

Maintainer

+

yuanyuancin
lvjing2

+

PMC (Project Management Committee)

+

暂无

+

Committer

+

暂无

+

Contributor

+

暂无

+
+ +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/runtime/_print/index.html b/docs/public/docs/contribution-guidelines/runtime/_print/index.html index e1e152b67..4980f8aa6 100644 --- a/docs/public/docs/contribution-guidelines/runtime/_print/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/_print/index.html @@ -1,62 +1,443 @@ -多模块运行时适配或最佳实践 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +多模块运行时适配或最佳实践 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

这是本节的多页打印视图。 -点击此处打印.

返回本页常规视图.

多模块运行时适配或最佳实践

1 - log4j2 的多模块化适配

为什么需要做适配

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

普通应用 log4j2 的初始化

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 -org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

该方法会根据 loggerContext 来判断是否已经初始化过了

这里在多模块下会存在问题一

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

  1. 获取到日志配置文件
  2. 解析日志配置文件里的变量值 -这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的

获取日志配置文件

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

解析日志配置值

配置文件里有一些变量,例如这些变量

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

预期多模块合并下的日志

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

多模块适配点

  1. getLoggerContext() 能拿到模块自身的 LoggerContext -

  2. 需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 -b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

模块改造方式

详细查看源码

2 - ehcache 的多模块化最佳实践

为什么需要最佳实践

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

最佳实践的几个要求

  1. 基座里必须引入 ehcache,模块里复用基座

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 -image.png

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, -image.png + + + +

+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

多模块运行时适配或最佳实践

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - log4j2 的多模块化适配

+ +

为什么需要做适配

+

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

+

普通应用 log4j2 的初始化

+

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 +org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

+

+

该方法会根据 loggerContext 来判断是否已经初始化过了

+
+

这里在多模块下会存在问题一

+

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

+
+

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

+
    +
  1. 获取到日志配置文件
  2. +
  3. 解析日志配置文件里的变量值 +这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的
  4. +
+

获取日志配置文件

+

+

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

+

+

解析日志配置值

+

配置文件里有一些变量,例如这些变量

+

+

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

+ + + + + + + + + + + + + + + + + + + + +
变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中
+

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

+

预期多模块合并下的日志

+

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

+

多模块适配点

+
    +
  1. +

    getLoggerContext() 能拿到模块自身的 LoggerContext +

    +
  2. +
  3. +

    需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    +

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 +b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

    +
  4. +
+

模块改造方式

+

详细查看源码

+ +
+ + + + + + + + + + + +
+ +

2 - ehcache 的多模块化最佳实践

+ +

为什么需要最佳实践

+

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

+

最佳实践的几个要求

+
    +
  1. 基座里必须引入 ehcache,模块里复用基座
  2. +
+

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 +image.png

+

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, +image.png 如果 net.sf.ehcache.CacheManager 是。这里会走到 java native 方法上做判断,从当前类所在的 ClassLoader 里查找 net.sf.ehcache.CacheManager 类,所以基座里必须引入这个依赖,否则会报 ClassNotFound 的错误。 -image.png

  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, -image.png -image.png +image.png

+
    +
  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)
  2. +
+

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, +image.png +image.png 这里在 new 对象时,需要先获得对象所属类的 CacheManager 是基座的 CacheManager。这里也不能讲 CacheManager 由模块 compile 引入,否则会出现一个类由多个不同 ClassLoader 引入导致的问题。 -image.png

所以结论是,这里需要全部委托给基座加载。

最佳实践的方式

  1. 模块 ehcache 排包瘦身委托给基座加载
  2. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  3. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
 <plugin>
-    <groupId>com.google.code.maven-replacer-plugin</groupId>
-    <artifactId>replacer</artifactId>
-    <version>1.5.3</version>
-    <executions>
-        <!-- 打包前进行替换 -->
-        <execution>
-            <phase>prepare-package</phase>
-            <goals>
-                <goal>replace</goal>
-            </goals>
-        </execution>
-    </executions>
-    <configuration>
-        <!-- 自动识别到项目target文件夹 -->
-        <basedir>${build.directory}</basedir>
-        <!-- 替换的文件所在目录规则 -->
-        <includes>
-            <include>classes/j2cache/*.properties</include>
-        </includes>
-        <replacements>
-            <replacement>
-                <token>ehcache.ehcache.name=f6-cache</token>
-                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
-            </replacement>
-
-        </replacements>
-    </configuration>
-</plugin>
-
  1. 需要把 FactoryBean 的 shared 设置成 false
@Bean
-    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
-        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
-
-        // 需要把 factoryBean 的 share 属性设置成 false
-        factoryBean.setShared(true);
-//        factoryBean.setShared(false);
-        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
-        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
-        return factoryBean;
-    }
-

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 -image.png -image.png

最佳实践的样例

样例工程请参考这里

- - \ No newline at end of file +image.png

+

所以结论是,这里需要全部委托给基座加载。

+

最佳实践的方式

+
    +
  1. 模块 ehcache 排包瘦身委托给基座加载
  2. +
  3. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  4. +
  5. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
  6. +
+
 <plugin>
+    <groupId>com.google.code.maven-replacer-plugin</groupId>
+    <artifactId>replacer</artifactId>
+    <version>1.5.3</version>
+    <executions>
+        <!-- 打包前进行替换 -->
+        <execution>
+            <phase>prepare-package</phase>
+            <goals>
+                <goal>replace</goal>
+            </goals>
+        </execution>
+    </executions>
+    <configuration>
+        <!-- 自动识别到项目target文件夹 -->
+        <basedir>${build.directory}</basedir>
+        <!-- 替换的文件所在目录规则 -->
+        <includes>
+            <include>classes/j2cache/*.properties</include>
+        </includes>
+        <replacements>
+            <replacement>
+                <token>ehcache.ehcache.name=f6-cache</token>
+                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
+            </replacement>
+
+        </replacements>
+    </configuration>
+</plugin>
+
    +
  1. 需要把 FactoryBean 的 shared 设置成 false
  2. +
+
@Bean
+    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
+        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
+
+        // 需要把 factoryBean 的 share 属性设置成 false
+        factoryBean.setShared(true);
+//        factoryBean.setShared(false);
+        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
+        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
+        return factoryBean;
+    }
+

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 +image.png +image.png

+

最佳实践的样例

+

样例工程请参考这里

+ +
+ + + + + + + + + + + +
+ +

3 -

+ + +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html b/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html index 474b625bb..950518143 100644 --- a/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html @@ -1,70 +1,573 @@ -ehcache 的多模块化最佳实践 | SOFAServerless + + + + + + + + + + + + + + + + + + +ehcache 的多模块化最佳实践 | SOFAServerless + + + + + + + + + + + + + + - - +最佳实践的方式 模块 ehcache 排包瘦身委托给基座加载 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName <plugin> <groupId>com."/> + + + + + + + + + + + + + + + + + + + -

ehcache 的多模块化最佳实践

为什么需要最佳实践

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

最佳实践的几个要求

  1. 基座里必须引入 ehcache,模块里复用基座

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 -image.png

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, -image.png + + + +

+ +
+
+
+
+ + +
+ + + + + +
+

ehcache 的多模块化最佳实践

+ + +

为什么需要最佳实践

+

CacheManager 初始化的时候存在共用 static 变量,多应用使用相同的 ehcache name,导致缓存互相覆盖。

+

最佳实践的几个要求

+
    +
  1. 基座里必须引入 ehcache,模块里复用基座
  2. +
+

在 springboot 里 ehcache 的初始化需要通过 Spring 里定义的 EhCacheCacheConfiguration 来创建,由于 EhCacheCacheConfiguration 是属于 Spring, Spring 统一放在基座里。 +image.png

+

这里在初始化的时候,在做 Bean 初始化的条件判断时会走到类的检验, +image.png 如果 net.sf.ehcache.CacheManager 是。这里会走到 java native 方法上做判断,从当前类所在的 ClassLoader 里查找 net.sf.ehcache.CacheManager 类,所以基座里必须引入这个依赖,否则会报 ClassNotFound 的错误。 -image.png

  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, -image.png -image.png +image.png

+
    +
  1. 模块里将引入的 ehcache 排包掉(scope设置成 provide,或者使用自动瘦身能力)
  2. +
+

模块使用自己 引入的 ehcache,照理可以避免共用基座 CacheManager 类里的 static 变量,而导致报错的问题。但是实际测试发现,模块安装的时候,在初始化 enCacheCacheManager 时, +image.png +image.png 这里在 new 对象时,需要先获得对象所属类的 CacheManager 是基座的 CacheManager。这里也不能讲 CacheManager 由模块 compile 引入,否则会出现一个类由多个不同 ClassLoader 引入导致的问题。 -image.png

所以结论是,这里需要全部委托给基座加载。

最佳实践的方式

  1. 模块 ehcache 排包瘦身委托给基座加载
  2. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  3. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
 <plugin>
-    <groupId>com.google.code.maven-replacer-plugin</groupId>
-    <artifactId>replacer</artifactId>
-    <version>1.5.3</version>
-    <executions>
-        <!-- 打包前进行替换 -->
-        <execution>
-            <phase>prepare-package</phase>
-            <goals>
-                <goal>replace</goal>
-            </goals>
-        </execution>
-    </executions>
-    <configuration>
-        <!-- 自动识别到项目target文件夹 -->
-        <basedir>${build.directory}</basedir>
-        <!-- 替换的文件所在目录规则 -->
-        <includes>
-            <include>classes/j2cache/*.properties</include>
-        </includes>
-        <replacements>
-            <replacement>
-                <token>ehcache.ehcache.name=f6-cache</token>
-                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
-            </replacement>
-
-        </replacements>
-    </configuration>
-</plugin>
-
  1. 需要把 FactoryBean 的 shared 设置成 false
@Bean
-    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
-        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
-
-        // 需要把 factoryBean 的 share 属性设置成 false
-        factoryBean.setShared(true);
-//        factoryBean.setShared(false);
-        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
-        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
-        return factoryBean;
-    }
-

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 -image.png -image.png

最佳实践的样例

样例工程请参考这里

-

最后修改 November 19, 2023: add ehcache principle (79628fc8)
- - \ No newline at end of file +image.png

+

所以结论是,这里需要全部委托给基座加载。

+

最佳实践的方式

+
    +
  1. 模块 ehcache 排包瘦身委托给基座加载
  2. +
  3. 如果多个模块里有多个相同的 cacheName,需要修改 cacheName 为不同值。
  4. +
  5. 如果不想改代码的方式修改 cache name,可以通过打包插件的方式动态替换 cacheName
  6. +
+
 <plugin>
+    <groupId>com.google.code.maven-replacer-plugin</groupId>
+    <artifactId>replacer</artifactId>
+    <version>1.5.3</version>
+    <executions>
+        <!-- 打包前进行替换 -->
+        <execution>
+            <phase>prepare-package</phase>
+            <goals>
+                <goal>replace</goal>
+            </goals>
+        </execution>
+    </executions>
+    <configuration>
+        <!-- 自动识别到项目target文件夹 -->
+        <basedir>${build.directory}</basedir>
+        <!-- 替换的文件所在目录规则 -->
+        <includes>
+            <include>classes/j2cache/*.properties</include>
+        </includes>
+        <replacements>
+            <replacement>
+                <token>ehcache.ehcache.name=f6-cache</token>
+                <value>ehcache.ehcache.name=f6-${parent.artifactId}-cache</value>
+            </replacement>
+
+        </replacements>
+    </configuration>
+</plugin>
+
    +
  1. 需要把 FactoryBean 的 shared 设置成 false
  2. +
+
@Bean
+    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
+        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
+
+        // 需要把 factoryBean 的 share 属性设置成 false
+        factoryBean.setShared(true);
+//        factoryBean.setShared(false);
+        factoryBean.setCacheManagerName("biz1EhcacheCacheManager");
+        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
+        return factoryBean;
+    }
+

否则会进入这段逻辑,初始化 CacheManager 的static 变量 instance. 该变量如果有值,且如果模块里 shared 也是ture 的化,就会重新复用 CacheManager 的 instance,从而拿到基座的 CacheManager, 从而报错。 +image.png +image.png

+

最佳实践的样例

+

样例工程请参考这里

+ + +
+ + + + + + +
+ + +
+
+ 最后修改 November 19, 2023: add ehcache principle (79628fc8) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/runtime/index.html b/docs/public/docs/contribution-guidelines/runtime/index.html index fc5a10459..3a9459bd5 100644 --- a/docs/public/docs/contribution-guidelines/runtime/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/index.html @@ -1,12 +1,510 @@ -多模块运行时适配或最佳实践 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +多模块运行时适配或最佳实践 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

多模块运行时适配或最佳实践

-

最后修改 November 17, 2023: add ehcache (bf88d79a)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

多模块运行时适配或最佳实践

+ + + +
+ + + + + + + + +
+ + +
+
+ log4j2 的多模块化适配 +
+

+
+ + +
+
+ ehcache 的多模块化最佳实践 +
+

+
+ + +
+
+ +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 November 17, 2023: add ehcache (bf88d79a) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/runtime/logback/index.html b/docs/public/docs/contribution-guidelines/runtime/logback/index.html new file mode 100644 index 000000000..827b6685a --- /dev/null +++ b/docs/public/docs/contribution-guidelines/runtime/logback/index.html @@ -0,0 +1,480 @@ + + + + + + + + + + + + + + + + + + + + +SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

+ + + + +
+ + + + + + +
+ + +
+ + +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/runtime/logj42/index.html b/docs/public/docs/contribution-guidelines/runtime/logj42/index.html index 3b0e1a5b3..aa5cd8a11 100644 --- a/docs/public/docs/contribution-guidelines/runtime/logj42/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/logj42/index.html @@ -1,4 +1,25 @@ -log4j2 的多模块化适配 | SOFAServerless + + + + + + + + + + + + + + + + + + +log4j2 的多模块化适配 | SOFAServerless + + + + + + + + + + + + + + - - +变量写法 代码逻辑地址 ${bundle:application:logging.file.path} org."/> + + + + + + + + + + + + + + + + + + + -

log4j2 的多模块化适配

为什么需要做适配

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

普通应用 log4j2 的初始化

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 -org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

该方法会根据 loggerContext 来判断是否已经初始化过了

这里在多模块下会存在问题一

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

  1. 获取到日志配置文件
  2. 解析日志配置文件里的变量值 -这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的

获取日志配置文件

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

解析日志配置值

配置文件里有一些变量,例如这些变量

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

预期多模块合并下的日志

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

多模块适配点

  1. getLoggerContext() 能拿到模块自身的 LoggerContext -

  2. 需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 -b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

模块改造方式

详细查看源码

-

最后修改 November 17, 2023: add ehcache (bf88d79a)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

log4j2 的多模块化适配

+ + +

为什么需要做适配

+

原生 log4j2 在多模块下,模块没有独立打印的日志目录,统一打印到基座目录里,导致日志和对应的监控无法隔离。这里做适配的目的就是要让模块能有独立的日志目录。

+

普通应用 log4j2 的初始化

+

在 Spring 启动前,log4j2 会使用默认值初始化一次各种 logContext 和 Configuration,然后在 Spring 启动过程中,监听 Spring 事件进行初始化 +org.springframework.boot.context.logging.LoggingApplicationListener,这里会调用到 Log4j2LoggingSystem.initialize 方法

+

+

该方法会根据 loggerContext 来判断是否已经初始化过了

+
+

这里在多模块下会存在问题一

+

这里的 getLoggerContext 是根据 org.apache.logging.log4j.LogManager 所在 classLoader 来获取 LoggerContext。根据某个类所在 ClassLoader 来提取 LoggerContext 在多模块化里会存在不稳定,因为模块一些类可以设置为委托给基座加载,所以模块里启动的时候,可能拿到的 LoggerContext 是基座的,导致这里 isAlreadyInitialized 直接返回,导致模块的 log4j2 日志无法进一步根据用户配置文件配置。

+
+

如果没初始化过,则会进入 super.initialize, 这里需要做两部分事情:

+
    +
  1. 获取到日志配置文件
  2. +
  3. 解析日志配置文件里的变量值 +这两部分在多模块里都可能存在问题,先看下普通应用过程是如何完成这两步的
  4. +
+

获取日志配置文件

+

+

可以看到是通过 ResourceUtils.getURL 获取的 location 对应日志配置文件的 url,这里通过获取到当前线程上下文 ClassLoader 来获取 URL,这在多模块下没有问题(因为每个模块启动时线程上下文已经是 模块自身的 ClassLoader )。

+

+

解析日志配置值

+

配置文件里有一些变量,例如这些变量

+

+

这些变量的解析逻辑在 org.apache.logging.log4j.core.lookup.AbstractLookup 的具体实现里,包括

+ + + + + + + + + + + + + + + + + + + + +
变量写法代码逻辑地址
${bundle:application:logging.file.path}org.apache.logging.log4j.core.lookup.ResourceBundleLookup根据 ResourceBundleLookup 所在 ClassLoader 提前到 application.properties, 读取里面的值
${ctx:logging.file.path}org.apache.logging.log4j.core.lookup.ContextMapLookup根据 LoggerContext 上下文 ThreadContex 存储的值来提起,这里需要提前把 applicaiton.properties 的值设置到 ThreadContext 中
+

根据上面判断通过 bundle 的方式配置在多模块里不可行,因为 ResourceBundleLookup 可能只存在于基座中,导致始终只能拿到基座的 application.properties,导致模块的日志配置路径与基座相同,模块日志都打到基座中。所以需要改造成使用 ContextMapLookup。

+

预期多模块合并下的日志

+

基座与模块都能使用独立的日志配置、配置值,完全独立。但由于上述分析中,存在两处可能导致模块无法正常初始化的逻辑,故这里需要多 log4j2 进行适配。

+

多模块适配点

+
    +
  1. +

    getLoggerContext() 能拿到模块自身的 LoggerContext +

    +
  2. +
  3. +

    需要调整成使用 ContextMapLookup,从而模块日志能获取到模块应用名,日志能打印到模块目录里

    +

    a. 模块启动时将 application.properties 的值设置到 ThreadContext 中 +b. 日志配置时,只能使用 ctx:xxx:xxx 的配置方式

    +
  4. +
+

模块改造方式

+

详细查看源码

+ + +
+ + + + + + +
+ + +
+
+ 最后修改 November 17, 2023: add ehcache (bf88d79a) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/sofa-ark/_print/index.html b/docs/public/docs/contribution-guidelines/sofa-ark/_print/index.html index 34bcb40aa..a60642c46 100644 --- a/docs/public/docs/contribution-guidelines/sofa-ark/_print/index.html +++ b/docs/public/docs/contribution-guidelines/sofa-ark/_print/index.html @@ -1,8 +1,240 @@ -SOFAArk 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +SOFAArk 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/contribution-guidelines/sofa-ark/index.html b/docs/public/docs/contribution-guidelines/sofa-ark/index.html index 7f96840fa..ed691a7e6 100644 --- a/docs/public/docs/contribution-guidelines/sofa-ark/index.html +++ b/docs/public/docs/contribution-guidelines/sofa-ark/index.html @@ -1,12 +1,494 @@ -SOFAArk 技术文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +SOFAArk 技术文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

SOFAArk 技术文档

SOFAArk 2.0 介绍
Ark 容器启动流程
Ark 容器插件机制
Ark 容器类加载机制
打包插件源码解析
启动过程源码解析
动态热部署源码解析
类委托加载源码解析
多 Web 应用合并部署源码解析


-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

SOFAArk 技术文档

+ + +

SOFAArk 2.0 介绍
+Ark 容器启动流程
+Ark 容器插件机制
+Ark 容器类加载机制
+打包插件源码解析
+启动过程源码解析
+动态热部署源码解析
+类委托加载源码解析
+多 Web 应用合并部署源码解析

+
+ +
+ + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/faq/_print/index.html b/docs/public/docs/faq/_print/index.html index 22252b1c3..9492c0483 100644 --- a/docs/public/docs/faq/_print/index.html +++ b/docs/public/docs/faq/_print/index.html @@ -1,9 +1,324 @@ -常见 FAQ | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +常见 FAQ | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - 如果模块独立引入 SpringBoot 框架部分会怎样?

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

CreateSpringFactoriesInstances

image.png

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener
-

所以模块框架这部分需要委托给基座加载。



- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

常见 FAQ

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - 常见问题列表

+ +

问题 1-1:模块 compile 引入 springboot 依赖,模块安装时报错

+
java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+
解决方式
+

模块需要做好瘦身,参考这里:模块瘦身

+

问题 1-2:模块安装找不到 ServerlessApplicationListener

+

报错信息如下:

+
com.alipay.sofa.ark.exception.ArkLoaderException: [ArkBiz Loader] module1:1.0-SNAPSHOT : can not load class: com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+
解决方式
+

请在模块里面添加如下依赖:

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>0.5.5</version>
+</dependency>
+

或者升级 sofa-serverless 版本到最新版本

+

问题 1-3: 通过 go install 无法安装 arkctl

+

执行如下命令,报错

+
go install serverless.alipay.com/sofa-serverless/v1/arkctl@latest
+

报错信息如下:

+
go: serverless.alipay.com/sofa-serverless/v1/arkctl@latest: module serverless.alipay.com/sofa-serverless/v1/arkctl: Get "https://proxy.golang.org/serverless.alipay.com/sofa-serverless/v1/arkctl/@v/list": dial tcp 142.251.42.241:443: i/o timeout
+
解决方式
+

arkctl 是作为 sofa-serverless 子目录的方式存在的,所以没法直接 go get,可以从这下面下载执行文件, 请参考安装 arkctl

+

问题 1-4:模块安装报 Master biz environment is null

+

解决方式,升级 sofa-serverless 版本到最新版本

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>${最新版本号}</version>
+</dependency>
+

问题 1-5:模块静态合并部署无法从制定的目录里找到模块包

+

解决方式,升级 sofa-serverless 版本到最新版本

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>${最新版本号}</version>
+</dependency>
+
+
+ + + + + + + + + + + +
+ +

2 - 如果模块独立引入 SpringBoot 框架部分会怎样?

+ +

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

+

CreateSpringFactoriesInstances

+

image.png

+

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

+
Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener
+

所以模块框架这部分需要委托给基座加载。

+
+
+ +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/faq/faq/index.html b/docs/public/docs/faq/faq/index.html new file mode 100644 index 000000000..dcf1af776 --- /dev/null +++ b/docs/public/docs/faq/faq/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + +常见问题列表 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

常见问题列表

+ + +

问题 1-1:模块 compile 引入 springboot 依赖,模块安装时报错

+
java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+
解决方式
+

模块需要做好瘦身,参考这里:模块瘦身

+

问题 1-2:模块安装找不到 ServerlessApplicationListener

+

报错信息如下:

+
com.alipay.sofa.ark.exception.ArkLoaderException: [ArkBiz Loader] module1:1.0-SNAPSHOT : can not load class: com.alipay.sofa.serverless.common.spring.ServerlessApplicationListener
+
解决方式
+

请在模块里面添加如下依赖:

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>0.5.5</version>
+</dependency>
+

或者升级 sofa-serverless 版本到最新版本

+

问题 1-3: 通过 go install 无法安装 arkctl

+

执行如下命令,报错

+
go install serverless.alipay.com/sofa-serverless/v1/arkctl@latest
+

报错信息如下:

+
go: serverless.alipay.com/sofa-serverless/v1/arkctl@latest: module serverless.alipay.com/sofa-serverless/v1/arkctl: Get "https://proxy.golang.org/serverless.alipay.com/sofa-serverless/v1/arkctl/@v/list": dial tcp 142.251.42.241:443: i/o timeout
+
解决方式
+

arkctl 是作为 sofa-serverless 子目录的方式存在的,所以没法直接 go get,可以从这下面下载执行文件, 请参考安装 arkctl

+

问题 1-4:模块安装报 Master biz environment is null

+

解决方式,升级 sofa-serverless 版本到最新版本

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>${最新版本号}</version>
+</dependency>
+

问题 1-5:模块静态合并部署无法从制定的目录里找到模块包

+

解决方式,升级 sofa-serverless 版本到最新版本

+
<dependency>
+    <groupId>com.alipay.sofa.serverless</igroupId>
+    <artifactId>sofa-serverless-app-starter</artifactId>
+    <version>${最新版本号}</version>
+</dependency>
+
+ +
+ + + + + + +
+ + +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/faq/import-full-springboot-in-module/index.html b/docs/public/docs/faq/import-full-springboot-in-module/index.html index 752753cd1..02a1b7230 100644 --- a/docs/public/docs/faq/import-full-springboot-in-module/index.html +++ b/docs/public/docs/faq/import-full-springboot-in-module/index.html @@ -1,29 +1,511 @@ -如果模块独立引入 SpringBoot 框架部分会怎样? | SOFAServerless + + + + + + + + + + + + + + + + + + +如果模块独立引入 SpringBoot 框架部分会怎样? | SOFAServerless + + + + + + + + + + + + + + - - +Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener 所以模块框架这部分需要委托给基座加载。"/> + + + + + + + + + + + + + + + + + + + -

如果模块独立引入 SpringBoot 框架部分会怎样?

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

CreateSpringFactoriesInstances

image.png

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener
-

所以模块框架这部分需要委托给基座加载。



-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

如果模块独立引入 SpringBoot 框架部分会怎样?

+ + +

由于多模块运行时的逻辑在基座引入和加载,例如一些 Spring 的 Listener。如果模块启动使用完全自己的 SpringBoot,则会出现一些类的转换或赋值判断失败,例如:

+

CreateSpringFactoriesInstances

+

image.png

+

name = ‘com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener’, ClassUtils.forName 获取到的是从基座 ClassLoader 的类
image.png
而 type 是模块启动时加载的,也就是使用模块 BizClassLoader 加载。
image.png
此时这里做 isAssignable 判断,则会报错。

+
Cannot instantiate interface org.springframework.context.ApplicationListener : com.alipay.sofa.ark.springboot.listener.ArkApplicationStartListener
+

所以模块框架这部分需要委托给基座加载。

+
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/faq/index.html b/docs/public/docs/faq/index.html index 635de6135..355fa2ab2 100644 --- a/docs/public/docs/faq/index.html +++ b/docs/public/docs/faq/index.html @@ -1,12 +1,502 @@ -常见 FAQ | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +常见 FAQ | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

常见 FAQ

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

常见 FAQ

+ + + +
+ + + + + + + + +
+ + +
+
+ 常见问题列表 +
+

+
+ + +
+
+ 如果模块独立引入 SpringBoot 框架部分会怎样? +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/index.html b/docs/public/docs/index.html index 63d05bdf5..f05d152ab 100644 --- a/docs/public/docs/index.html +++ b/docs/public/docs/index.html @@ -1,12 +1,537 @@ -产品文档 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +产品文档 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

产品文档

-

最后修改 November 3, 2023: Revert "update docs" (d7d8e5c4)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

产品文档

+ + + + +
+ + + + + + + + +
+ + +
+
+ 产品介绍 +
+

+
+ + +
+
+ 快速开始 +
+

+
+ + +
+
+ 视频教程 +
+

+
+ + +
+
+ 用户手册 +
+

+
+ + +
+
+ 参与社区 +
+

+
+ + +
+
+ 常见 FAQ +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 November 3, 2023: Revert "update docs" (d7d8e5c4) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/introduction/_print/index.html b/docs/public/docs/introduction/_print/index.html index 4e9a9df2e..ae9ba8722 100644 --- a/docs/public/docs/introduction/_print/index.html +++ b/docs/public/docs/introduction/_print/index.html @@ -1,22 +1,625 @@ -产品介绍 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +产品介绍 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - 简介与适用场景

简介

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

适用场景

痛点 1:应用构建发布慢或者 SDK 升级繁琐

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

痛点 2:长尾应用资源成本高

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本

这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

痛点 3:企业研发协作效率低

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。

痛点 4:难以沉淀业务资产提高中台效率

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。

痛点 5:微服务演进成本高

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进



2 - 行业背景

微服务的问题

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

基础设施复杂

认知负载高

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

运维负担重

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

  1. 重要功能版本发布
  2. 修复紧急 bug
  3. 遇到重大安全漏洞

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

对于业务研发人员

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

对于基础设施人员

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

启动慢

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

发布效率低

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

协作与资源成本高

单体应用/大应用过大

image.png

多人协作阻塞

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

变更影响面大,风险高

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

小应用过多

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

资源成本高

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

长期维护成本

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

问题必然性

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

如何合理拆分微服务

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

微服务的拆分与业务和组织发展敏捷度不一致

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

不同体量企业面对的问题

image.png

行业尝试的解法

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. 只解决其中部分问题
  3. 存量应用接入改造成本高

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。



3 - 架构介绍

3.1 - 架构原理

模块化应用架构

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

模块的定义

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

  1. SpringBoot 打包生成的 jar 包
  2. 一个模块: 一个 SpringContext + 一个 ClassLoader
  3. 热部署(升级的时候不需要启动进程)

模块的隔离与共享

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

  1. Ark Container 提供多 ClassLoader 运行环境
  2. Arklet 来管理模块生命周期
  3. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  4. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  5. SpringContext Manager 提供 Bean 与服务发现调用机制
  6. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader

image.png

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

隔离方面

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

共享方面

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

模块间通信

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

模块的可演进

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

  1. 模块不占资源所以拆分没有资源成本
  2. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  3. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  4. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

模块化架构的优势

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

平台架构

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

image.png
组件视图

在整个平台里,需要四个组件:

  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  3. 控制面组件 ModuleController
    1. ModuleDeployment 提供模块发布与运维能力
    2. ModuleScheduler 提供模块调度能力
    3. ModuleScaler 提供模块伸缩能力

3.2 - 基座与模块间类委托加载原理介绍

多模块间类委托加载

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

当前类委托加载机制

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. 模块可以扫描到基座里的所有类:
    • 优势:模块可以引入较少依赖
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
  3. 模块不能扫描到基座里的任何资源:
    • 优势:不会与基座重复初始化相同的 Bean
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
  4. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。

使用时需要注意事项

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

类委托的最佳实践

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

强制委托加载

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

使用方法

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

优点

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

缺点

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

自定义委托加载

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. 基座里预置一些公共依赖(可选)。
  3. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座:
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
            <plugin>
-                <groupId>com.alipay.sofa</groupId>
-                <artifactId>sofa-ark-maven-plugin</artifactId>
-                <configuration> 
-                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
-                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
-                    <declaredMode>true</declaredMode>
-                </configuration>
-            </plugin>
-

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

  1. 只有模块声明过的依赖才可以委托给基座加载。

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

优点

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

缺点

对模块瘦身的依赖较强。

对比与总结

依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低

结论:推荐自定义委托加载方式

  1. 模块自定义委托加载 + 模块瘦身。
  2. 模块开启 declaredMode。
  3. 基座预置依赖。

declaredMode 开启方式

开启条件

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

# 如果是 SOFABoot,则:
-# 配置健康检查跳过 JVM 服务检查
-com.alipay.sofa.boot.skip-jvm-reference-health-check=true
-# 忽略未解析的占位符
-com.alipay.sofa.ignore.unresolvable.placeholders=true
-

开启方式

模块打包插件里增加如下配置:
image.png

开启后的副作用

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。


- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

产品介绍

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - 简介与适用场景

+ +

简介

+

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

+

适用场景

+

痛点 1:应用构建发布慢或者 SDK 升级繁琐

+

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

+ +

痛点 2:长尾应用资源成本高

+

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本
+
+这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

+

痛点 3:企业研发协作效率低

+

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。
+

+

痛点 4:难以沉淀业务资产提高中台效率

+

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。
+

+

痛点 5:微服务演进成本高

+

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

+

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

+

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进
+

+
+
+ +
+ + + + + + + + + + + +
+ +

2 - 行业背景

+ +

微服务的问题

+

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

+

基础设施复杂

+

认知负载高

+

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

+

运维负担重

+

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

+
    +
  1. 重要功能版本发布
  2. +
  3. 修复紧急 bug
  4. +
  5. 遇到重大安全漏洞
  6. +
+

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

+
对于业务研发人员
+

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

+
对于基础设施人员
+

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

+

启动慢

+

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

+

发布效率低

+

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

+

协作与资源成本高

+

单体应用/大应用过大

+

image.png

+
多人协作阻塞
+

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

+
变更影响面大,风险高
+

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

+

小应用过多

+

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

+
资源成本高
+

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

+
长期维护成本
+

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

+

问题必然性

+

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

+

如何合理拆分微服务

+

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

+

微服务的拆分与业务和组织发展敏捷度不一致

+

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

+

不同体量企业面对的问题

+

image.png

+

行业尝试的解法

+

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

+
    +
  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. +
  3. 只解决其中部分问题
  4. +
  5. 存量应用接入改造成本高
  6. +
+

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。

+
+
+ +
+ + + + + + + + + + + +
+ +

3 - 架构介绍

+ + +
+ + + + + + + + + + + + + + + + + + + +
+ +

3.1 - 架构原理

+ +

模块化应用架构

+

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

+

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

+

模块的定义

+

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

+
    +
  1. SpringBoot 打包生成的 jar 包
  2. +
  3. 一个模块: 一个 SpringContext + 一个 ClassLoader
  4. +
  5. 热部署(升级的时候不需要启动进程)
  6. +
+

模块的隔离与共享

+

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

+
    +
  1. Ark Container 提供多 ClassLoader 运行环境
  2. +
  3. Arklet 来管理模块生命周期
  4. +
  5. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  6. +
  7. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  8. +
  9. SpringContext Manager 提供 Bean 与服务发现调用机制
  10. +
  11. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader
  12. +
+

image.png

+

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

+

隔离方面

+

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

+

共享方面

+

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

+

模块间通信

+

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

+

模块的可演进

+

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

+
    +
  1. 模块不占资源所以拆分没有资源成本
  2. +
  3. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  4. +
  5. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  6. +
  7. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式
  8. +
+

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

+

模块化架构的优势

+

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

+

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

+

平台架构

+

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

+

image.png
组件视图

+

在整个平台里,需要四个组件:

+
    +
  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. +
  3. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  4. +
  5. 控制面组件 ModuleController +
      +
    1. ModuleDeployment 提供模块发布与运维能力
    2. +
    3. ModuleScheduler 提供模块调度能力
    4. +
    5. ModuleScaler 提供模块伸缩能力
    6. +
    +
  6. +
+
+ +
+ + + + + + + + + + + +
+ +

3.2 - 基座与模块间类委托加载原理介绍

+ +

多模块间类委托加载

+

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

+

当前类委托加载机制

+

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

+
    +
  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. +
  3. 模块可以扫描到基座里的所有类: +
      +
    • 优势:模块可以引入较少依赖
    • +
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
    • +
    +
  4. +
  5. 模块不能扫描到基座里的任何资源: +
      +
    • 优势:不会与基座重复初始化相同的 Bean
    • +
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
    • +
    +
  6. +
  7. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。
  8. +
+

使用时需要注意事项

+

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

+

类委托的最佳实践

+

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

+

强制委托加载

+

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

+

使用方法

+

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

+

优点

+

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

+

缺点

+

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

+

自定义委托加载

+

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

+
    +
  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. +
  3. 基座里预置一些公共依赖(可选)。
  4. +
  5. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座: +
      +
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. +
    3. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
    4. +
    +
  6. +
+
            <plugin>
+                <groupId>com.alipay.sofa</groupId>
+                <artifactId>sofa-ark-maven-plugin</artifactId>
+                <configuration> 
+                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
+                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
+                    <declaredMode>true</declaredMode>
+                </configuration>
+            </plugin>
+

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

+
    +
  1. 只有模块声明过的依赖才可以委托给基座加载。
  2. +
+

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

+

优点

+

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

+

缺点

+

对模块瘦身的依赖较强。

+

对比与总结

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低
+

结论:推荐自定义委托加载方式

+
    +
  1. 模块自定义委托加载 + 模块瘦身。
  2. +
  3. 模块开启 declaredMode。
  4. +
  5. 基座预置依赖。
  6. +
+

declaredMode 开启方式

+

开启条件

+

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

+
# 如果是 SOFABoot,则:
+# 配置健康检查跳过 JVM 服务检查
+com.alipay.sofa.boot.skip-jvm-reference-health-check=true
+# 忽略未解析的占位符
+com.alipay.sofa.ignore.unresolvable.placeholders=true
+

开启方式

+

模块打包插件里增加如下配置:
image.png

+

开启后的副作用

+

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。

+
+ +
+ + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/introduction/architecture/_print/index.html b/docs/public/docs/introduction/architecture/_print/index.html index 02fb7df50..89fca5603 100644 --- a/docs/public/docs/introduction/architecture/_print/index.html +++ b/docs/public/docs/introduction/architecture/_print/index.html @@ -1,22 +1,452 @@ -架构介绍 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +架构介绍 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

1 - 架构原理

模块化应用架构

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

模块的定义

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

  1. SpringBoot 打包生成的 jar 包
  2. 一个模块: 一个 SpringContext + 一个 ClassLoader
  3. 热部署(升级的时候不需要启动进程)

模块的隔离与共享

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

  1. Ark Container 提供多 ClassLoader 运行环境
  2. Arklet 来管理模块生命周期
  3. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  4. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  5. SpringContext Manager 提供 Bean 与服务发现调用机制
  6. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader

image.png

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

隔离方面

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

共享方面

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

模块间通信

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

模块的可演进

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

  1. 模块不占资源所以拆分没有资源成本
  2. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  3. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  4. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

模块化架构的优势

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

平台架构

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

image.png
组件视图

在整个平台里,需要四个组件:

  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  3. 控制面组件 ModuleController
    1. ModuleDeployment 提供模块发布与运维能力
    2. ModuleScheduler 提供模块调度能力
    3. ModuleScaler 提供模块伸缩能力

2 - 基座与模块间类委托加载原理介绍

多模块间类委托加载

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

当前类委托加载机制

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. 模块可以扫描到基座里的所有类:
    • 优势:模块可以引入较少依赖
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
  3. 模块不能扫描到基座里的任何资源:
    • 优势:不会与基座重复初始化相同的 Bean
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
  4. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。

使用时需要注意事项

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

类委托的最佳实践

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

强制委托加载

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

使用方法

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

优点

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

缺点

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

自定义委托加载

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. 基座里预置一些公共依赖(可选)。
  3. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座:
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
            <plugin>
-                <groupId>com.alipay.sofa</groupId>
-                <artifactId>sofa-ark-maven-plugin</artifactId>
-                <configuration> 
-                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
-                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
-                    <declaredMode>true</declaredMode>
-                </configuration>
-            </plugin>
-

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

  1. 只有模块声明过的依赖才可以委托给基座加载。

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

优点

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

缺点

对模块瘦身的依赖较强。

对比与总结

依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低

结论:推荐自定义委托加载方式

  1. 模块自定义委托加载 + 模块瘦身。
  2. 模块开启 declaredMode。
  3. 基座预置依赖。

declaredMode 开启方式

开启条件

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

# 如果是 SOFABoot,则:
-# 配置健康检查跳过 JVM 服务检查
-com.alipay.sofa.boot.skip-jvm-reference-health-check=true
-# 忽略未解析的占位符
-com.alipay.sofa.ignore.unresolvable.placeholders=true
-

开启方式

模块打包插件里增加如下配置:
image.png

开启后的副作用

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。


- - \ No newline at end of file + + + +
+ +
+
+
+
+
+ + + + + +
+
+

+这是本节的多页打印视图。 +点击此处打印. +

+返回本页常规视图. +

+
+ + + +

架构介绍

+ + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +

1 - 架构原理

+ +

模块化应用架构

+

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

+

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

+

模块的定义

+

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

+
    +
  1. SpringBoot 打包生成的 jar 包
  2. +
  3. 一个模块: 一个 SpringContext + 一个 ClassLoader
  4. +
  5. 热部署(升级的时候不需要启动进程)
  6. +
+

模块的隔离与共享

+

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

+
    +
  1. Ark Container 提供多 ClassLoader 运行环境
  2. +
  3. Arklet 来管理模块生命周期
  4. +
  5. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  6. +
  7. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  8. +
  9. SpringContext Manager 提供 Bean 与服务发现调用机制
  10. +
  11. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader
  12. +
+

image.png

+

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

+

隔离方面

+

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

+

共享方面

+

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

+

模块间通信

+

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

+

模块的可演进

+

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

+
    +
  1. 模块不占资源所以拆分没有资源成本
  2. +
  3. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  4. +
  5. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  6. +
  7. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式
  8. +
+

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

+

模块化架构的优势

+

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

+

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

+

平台架构

+

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

+

image.png
组件视图

+

在整个平台里,需要四个组件:

+
    +
  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. +
  3. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  4. +
  5. 控制面组件 ModuleController +
      +
    1. ModuleDeployment 提供模块发布与运维能力
    2. +
    3. ModuleScheduler 提供模块调度能力
    4. +
    5. ModuleScaler 提供模块伸缩能力
    6. +
    +
  6. +
+
+ +
+ + + + + + + + + + + +
+ +

2 - 基座与模块间类委托加载原理介绍

+ +

多模块间类委托加载

+

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

+

当前类委托加载机制

+

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

+
    +
  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. +
  3. 模块可以扫描到基座里的所有类: +
      +
    • 优势:模块可以引入较少依赖
    • +
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
    • +
    +
  4. +
  5. 模块不能扫描到基座里的任何资源: +
      +
    • 优势:不会与基座重复初始化相同的 Bean
    • +
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
    • +
    +
  6. +
  7. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。
  8. +
+

使用时需要注意事项

+

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

+

类委托的最佳实践

+

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

+

强制委托加载

+

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

+

使用方法

+

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

+

优点

+

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

+

缺点

+

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

+

自定义委托加载

+

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

+
    +
  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. +
  3. 基座里预置一些公共依赖(可选)。
  4. +
  5. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座: +
      +
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. +
    3. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
    4. +
    +
  6. +
+
            <plugin>
+                <groupId>com.alipay.sofa</groupId>
+                <artifactId>sofa-ark-maven-plugin</artifactId>
+                <configuration> 
+                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
+                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
+                    <declaredMode>true</declaredMode>
+                </configuration>
+            </plugin>
+

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

+
    +
  1. 只有模块声明过的依赖才可以委托给基座加载。
  2. +
+

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

+

优点

+

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

+

缺点

+

对模块瘦身的依赖较强。

+

对比与总结

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低
+

结论:推荐自定义委托加载方式

+
    +
  1. 模块自定义委托加载 + 模块瘦身。
  2. +
  3. 模块开启 declaredMode。
  4. +
  5. 基座预置依赖。
  6. +
+

declaredMode 开启方式

+

开启条件

+

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

+
# 如果是 SOFABoot,则:
+# 配置健康检查跳过 JVM 服务检查
+com.alipay.sofa.boot.skip-jvm-reference-health-check=true
+# 忽略未解析的占位符
+com.alipay.sofa.ignore.unresolvable.placeholders=true
+

开启方式

+

模块打包插件里增加如下配置:
image.png

+

开启后的副作用

+

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。

+
+ +
+ + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + diff --git a/docs/public/docs/introduction/architecture/arch-principle/index.html b/docs/public/docs/introduction/architecture/arch-principle/index.html index d0472a040..af9b141f7 100644 --- a/docs/public/docs/introduction/architecture/arch-principle/index.html +++ b/docs/public/docs/introduction/architecture/arch-principle/index.html @@ -1,40 +1,580 @@ -架构原理 | SOFAServerless + + + + + + + + + + + + + + + + + + +架构原理 | SOFAServerless + + + + + + + + + + + + + + - - +Ark Container 提供多 ClassLoader 运行环境 Arklet 来管理模块生命周期 Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来 SOFAArk 默认委托加载机制,打通模块与基座类委托加载 SpringContext Manager 提供 Bean 与服务发现调用机制 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader 但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是"/> + + + + + + + + + + + + + + + + + + + -

架构原理

模块化应用架构

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

模块的定义

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

  1. SpringBoot 打包生成的 jar 包
  2. 一个模块: 一个 SpringContext + 一个 ClassLoader
  3. 热部署(升级的时候不需要启动进程)

模块的隔离与共享

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

  1. Ark Container 提供多 ClassLoader 运行环境
  2. Arklet 来管理模块生命周期
  3. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  4. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  5. SpringContext Manager 提供 Bean 与服务发现调用机制
  6. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader

image.png

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

隔离方面

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

共享方面

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

模块间通信

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

模块的可演进

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

  1. 模块不占资源所以拆分没有资源成本
  2. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  3. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  4. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

模块化架构的优势

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

平台架构

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

image.png
组件视图

在整个平台里,需要四个组件:

  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  3. 控制面组件 ModuleController
    1. ModuleDeployment 提供模块发布与运维能力
    2. ModuleScheduler 提供模块调度能力
    3. ModuleScaler 提供模块伸缩能力

-

最后修改 October 26, 2023: add styles (6b806506)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

架构原理

+ + +

模块化应用架构

+

为了解决这些问题,我们对应用同时做了横向和纵向的拆分。首先第一步纵向拆分:把应用拆分成基座业务两层,这两层分别对应两层的组织分工。基座小组与传统应用一样,负责机器维护、通用逻辑沉淀、业务架构治理,并为业务提供运行资源和环境。通过关注点分离的方式为业务屏蔽业务以下所有基础设施,聚焦在业务自身上。第二部我们将业务进行横向切分出多个模块,多个模块之间独立并行迭代互不影响,同时模块由于不包含基座部分,构建产物非常轻量,启动逻辑也只包含业务本身,所以启动快,具备秒级的验证能力,让模块开发得到极致的提效。
image.png
拆分之前,每个开发者可能感知从框架到中间件到业务公共部分到业务自身所有代码和逻辑,拆分后,团队的协作分工也从发生改变,研发人员分工出两种角色,基座和模块开发者,模块开发者不关系资源与容量,享受秒级部署验证能力,聚焦在业务逻辑自身上。
image.png

+

这里要重点看下我们是如何做这些纵向和横向切分的,切分是为了隔离,隔离是为了能够独立迭代、剥离不必要的依赖,然而如果只是隔离是没有共享相当于只是换了个部署的位置而已,很难有好的效果。所以我们除了隔离还有共享能力,所以这里需要聚焦在隔离与共享上来理解模块化架构背后的原理。

+

模块的定义

+

在这之前先看下这里的模块是什么?模块是通过原来应用减去基座部分得到的,这里的减法是通过设置模块里依赖的 scope 为 provided 实现的,
image.png
image.png
一个模块可以由这三点定义:

+
    +
  1. SpringBoot 打包生成的 jar 包
  2. +
  3. 一个模块: 一个 SpringContext + 一个 ClassLoader
  4. +
  5. 热部署(升级的时候不需要启动进程)
  6. +
+

模块的隔离与共享

+

模块通过 ClassLoader 隔离配置和代码,SpringContext 隔离 Bean 和服务,可以通过调用 Spring ApplicationContext 的start close 方法来动态启动和关闭服务。通过 SOFAArk 来共享模块和基座的配置和代码 Class,通过 SpringContext Manager 来共享多模块间的 Bean 和服务。
image.png
并且在 JVM 内通过

+
    +
  1. Ark Container 提供多 ClassLoader 运行环境
  2. +
  3. Arklet 来管理模块生命周期
  4. +
  5. Framework Adapter 将 SpringBoot 生命周期与模块生命周期关联起来
  6. +
  7. SOFAArk 默认委托加载机制,打通模块与基座类委托加载
  8. +
  9. SpringContext Manager 提供 Bean 与服务发现调用机制
  10. +
  11. 基座本质也是模块,拥有独立的 SpringContext 和 ClassLoader
  12. +
+

image.png

+

但是在 Java 领域模块化技术已经发展了20年了,为什么这里的模块化技术能够在蚂蚁内部规模化落地,这里的核心原因是
image.png
基于 SOFAArk 和 SpringContext Manager 的多模块能力,提供了低成本的使用方式。

+

隔离方面

+

对于其他的模块化技术,从隔离角度来看,JPMS 和 Spring Modulith 的隔离是通过自定义的规则来做限制的,Spring Modulith 还需要在单元测试里执行 verify 来做校验,隔离能力比较弱且一定程度上是比较 tricky 的,对于存量应用使用来说也是有不小改造成本的,甚至说是存量应用无法改造。而 SOFAArk 和 OSGI 一样采用 ClassLoader 和 SpringContext 的方式进行配置与代码、bean与服务的隔离,对原生应用的启动模式完全保持一致。

+

共享方面

+

SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JPMS、Spring Modulith 一样都需要在源模块和目标模块间定义导入导出列表或其他配置,这造成业务使用模块需要强感知和理解多模块的技术,使用成本是比较高的,而 SOFAArk 则定义了默认的类委托加载机制,和跨模块的 Bean 和服务发现机制,让业务不用改造的情况下能够使用多模块的能力。
这里额外提下,为什么基于 SOFAArk 的多模块化技术能提供这些默认的能力,而做到低成本的使用呢?这里主要的原因是因为我们对模块做了角色的区分,区分出了基座与模块,在这个核心原因基础上也对低成本使用这块比较重视,做了重要的设计考量和取舍。具体有哪些设计和取舍,可以查看技术实现文章。

+

模块间通信

+

模块间通信主要依托 SpringContext Manager 的 Bean 与服务发现调用机制提供基础能力,
image.png

+

模块的可演进

+

回顾背景里提到的几大问题,可以看到通过模块化架构的隔离与共享能力,可以解决掉基础设施复杂、多人协作阻塞、资源与长期维护成本高的问题,但还有微服务拆分与业务敏捷度不一致的问题未解决。
image.png
在这里我们通过降低微服务拆分的成本来解决,那么怎么降低微服务拆分成本呢?这里主要是在单体架构和微服务架构之间增加模块化架构

+
    +
  1. 模块不占资源所以拆分没有资源成本
  2. +
  3. 模块不包含业务公共部分和框架、中间件部分,所以模块没有长期的 sdk 升级维护成本
  4. +
  5. 模块自身也是 SpringBoot,我们提供工具辅助单体应用低成本拆分成模块应用
  6. +
  7. 模块具备灵活部署能力,可以合并部署在一个 JVM 内,也可拆除独立部署,这样模块可以按需低成本演进成微服务或回退会单体应用模式
  8. +
+

image.png
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉

+

模块化架构的优势

+

模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
image.png

+

与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
image.png

+

平台架构

+

只有应用架构还不够,需要从研发阶段到运维阶段到运行阶段都提供完整的配套能力,才能让模块化应用架构的优势真正触达到研发人员。
image.png
在研发阶段,需要提供基座接入能力,模块创建能力,更重要的是模块的本地快速构建与联调能力;在运维阶段,提供快速的模块发布能力,在模块发布基础上提供 A/B 测试和秒级扩缩容能力;在运行阶段,提供模块的可靠性能力,模块可观测、流量精细化控制、调度和伸缩能力。

+

image.png
组件视图

+

在整个平台里,需要四个组件:

+
    +
  1. 研发工具 Arkctl, 提供模块创建、快速联调测试等能力
  2. +
  3. 运行组件 SOFAArk, Arklet,提供模块运维、模块生命周期管理,多模块运行环境
  4. +
  5. 控制面组件 ModuleController +
      +
    1. ModuleDeployment 提供模块发布与运维能力
    2. +
    3. ModuleScheduler 提供模块调度能力
    4. +
    5. ModuleScaler 提供模块伸缩能力
    6. +
    +
  6. +
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 26, 2023: add styles (6b806506) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/introduction/architecture/class-delegation-principle/index.html b/docs/public/docs/introduction/architecture/class-delegation-principle/index.html index 22920a144..24ce6e335 100644 --- a/docs/public/docs/introduction/architecture/class-delegation-principle/index.html +++ b/docs/public/docs/introduction/architecture/class-delegation-principle/index.html @@ -1,4 +1,25 @@ -基座与模块间类委托加载原理介绍 | SOFAServerless + + + + + + + + + + + + + + + + + + +基座与模块间类委托加载原理介绍 | SOFAServerless + + + + + + + + + + + + + + - - +基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。 基座里预置一些公共依赖(可选)。 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座: 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。 biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIds 或 excludeArtifactIds <plugin> <groupId>com."/> + + + + + + + + + + + + + + + + + + + -

基座与模块间类委托加载原理介绍

多模块间类委托加载

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

当前类委托加载机制

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. 模块可以扫描到基座里的所有类:
    • 优势:模块可以引入较少依赖
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
  3. 模块不能扫描到基座里的任何资源:
    • 优势:不会与基座重复初始化相同的 Bean
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
  4. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。

使用时需要注意事项

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

类委托的最佳实践

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

强制委托加载

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

使用方法

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

优点

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

缺点

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

自定义委托加载

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. 基座里预置一些公共依赖(可选)。
  3. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座:
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
            <plugin>
-                <groupId>com.alipay.sofa</groupId>
-                <artifactId>sofa-ark-maven-plugin</artifactId>
-                <configuration> 
-                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
-                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
-                    <declaredMode>true</declaredMode>
-                </configuration>
-            </plugin>
-

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

  1. 只有模块声明过的依赖才可以委托给基座加载。

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

优点

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

缺点

对模块瘦身的依赖较强。

对比与总结

依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低

结论:推荐自定义委托加载方式

  1. 模块自定义委托加载 + 模块瘦身。
  2. 模块开启 declaredMode。
  3. 基座预置依赖。

declaredMode 开启方式

开启条件

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

# 如果是 SOFABoot,则:
-# 配置健康检查跳过 JVM 服务检查
-com.alipay.sofa.boot.skip-jvm-reference-health-check=true
-# 忽略未解析的占位符
-com.alipay.sofa.ignore.unresolvable.placeholders=true
-

开启方式

模块打包插件里增加如下配置:
image.png

开启后的副作用

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。


-

最后修改 October 27, 2023: fix doc (285936d5)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

基座与模块间类委托加载原理介绍

+ + +

多模块间类委托加载

+

SOFAArk 框架是基于多 ClassLoader 的通用类隔离方案,提供类隔离和应用的合并部署能力。本文档并不打算介绍 SOFAArk 类隔离的原理与机制,这里主要介绍多 ClassLoader 当前的最佳实践。
当前基座与模块部署在 JVM 上的 ClassLoader 模型如图:
image.png

+

当前类委托加载机制

+

当前一个模块在启动与运行时查找的类,有两个来源:当前模块本身,基座。这两个来源的理想优先级顺序是,优先从模块中查找,如果模块找不到再从基座中查找,但当前存在一些特例:

+
    +
  1. 当前定义了一份白名单,白名单范围内的依赖会强制使用基座里的依赖。
  2. +
  3. 模块可以扫描到基座里的所有类: +
      +
    • 优势:模块可以引入较少依赖
    • +
    • 劣势:模块会扫描到模块代码里不存在的类,例如会扫描到一些 AutoConfiguration,初始化时由于第四点扫描不到对应资源,所以会报错。
    • +
    +
  4. +
  5. 模块不能扫描到基座里的任何资源: +
      +
    • 优势:不会与基座重复初始化相同的 Bean
    • +
    • 劣势:模块启动如果需要基座的资源,会因为查找不到资源而报错,除非模块里显示引入(Maven 依赖 scope 不设置成 provided)
    • +
    +
  6. +
  7. 模块调用基座时,部分内部处理传入模块里的类名到基座,基座如果存在直接从基座 ClassLoader 查找模块传入的类,会查找不到。因为委托只允许模块委托给基座,从基座发起的类查找不会再次查找模块里的。
  8. +
+

使用时需要注意事项

+

模块要升级委托给基座的依赖时,需要让基座先升级,升级之后模块再升级。

+

类委托的最佳实践

+

类委托加载的准则是中间件相关的依赖需要放在同一个的 ClassLoader 里进行加载执行,达到这种方式的最佳实践有两种:

+

强制委托加载

+

由于中间件相关的依赖一般需要在同一个 ClassLoader 里加载运行,所以我们会制定一个中间件依赖的白名单,强制这些依赖委托给基座加载。

+

使用方法

+

application.properties 里增加配置 sofa.ark.plugin.export.class.enable=true

+

优点

+

模块开发者不需要感知哪些依赖属于需要强制加载由同一个 ClassLoader 加载的依赖。

+

缺点

+

白名单里要强制加载的依赖列表需要维护,列表的缺失需要更新基座,较为重要的升级需要推所有的基座升级。

+

自定义委托加载

+

模块里 pom 通过设置依赖的 scope 为 provided主动指定哪些要委托给基座加载。通过模块瘦身把与基座重复的依赖委托给基座加载,并在基座里预置中间件的依赖(可选,虽然模块暂时不会用到,但可以提前引入,以备后续模块需要引入的时候不需再发布基座即可引入)。这里:

+
    +
  1. 基座尽可能的沉淀通用的逻辑和依赖,特别是中间件相关以 xxx-alipay-sofa-boot-starter 命名的依赖。
  2. +
  3. 基座里预置一些公共依赖(可选)。
  4. +
  5. 模块里的依赖如果基座里面已经有定义,则模块里的依赖尽可能的委托给基座,这样模块会更轻(提供自动模块瘦身的工具)。模块里有两种途径设置为委托给基座: +
      +
    1. 依赖里的 scope 设置为 provided,注意通过 mvn dependency:tree 查看是否还有其他依赖设置成了 compile,需要所有的依赖引用的地方都设置为 provided。
    2. +
    3. biz 打包插件sofa-ark-maven-plugin里设置 excludeGroupIdsexcludeArtifactIds
    4. +
    +
  6. +
+
            <plugin>
+                <groupId>com.alipay.sofa</groupId>
+                <artifactId>sofa-ark-maven-plugin</artifactId>
+                <configuration> 
+                    <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
+                    <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
+                    <declaredMode>true</declaredMode>
+                </configuration>
+            </plugin>
+

通过 2.a 的方法需要确保所有声明的地方 scope 都设置为provided,通过2.b的方法只要指定一次即可,建议使用方法 2.b。

+
    +
  1. 只有模块声明过的依赖才可以委托给基座加载。
  2. +
+

模块启动的时候,Spring 框架会有一些扫描逻辑,这些扫描如果不做限制会查找到模块和基座的所有资源,导致一些模块明明不需要的功能尝试去初始化,从而报错。SOFAArk 2.0.3 之后新增了模块的 declaredMode, 来限制只有模块里声明过的依赖才可以委托给基座加载。只需在模块的打包插件的 Configurations 里增加 <declaredMode>true</declaredMode>即可。

+

优点

+

不需要维护 plugin 的强制加载列表,当部分需要由同一 ClassLoader 加载的依赖没有设置为统一加载时,可以修改模块就可以修复,不需要发布基座(除非基座确实依赖)。

+

缺点

+

对模块瘦身的依赖较强。

+

对比与总结

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
依赖缺失排查成本修复成本模块改造成本维护成本
强制加载类转换失败或类查找失败,成本中更新 plugin,发布基座,高
自定义委托加载类转换失败或类查找失败,成本中更新模块依赖,如果基座依赖不足,需要更新基座并发布,中
自定义委托加载 + 基座预置依赖 + 模块瘦身类转换失败或类查找失败,成本中更新模块依赖,设置为 provided,低
+

结论:推荐自定义委托加载方式

+
    +
  1. 模块自定义委托加载 + 模块瘦身。
  2. +
  3. 模块开启 declaredMode。
  4. +
  5. 基座预置依赖。
  6. +
+

declaredMode 开启方式

+

开启条件

+

declaredMode 的本意是让模块能合并部署到基座上,所以开启前需要确保模块能本地启动成功。
如果是 SOFABoot 应用且涉及到模块调用基座服务的,本地启动因为没有基座服务,可以通过在模块 application.properties 添加这两个参数进行跳过(SpringBoot 应用无需关心):

+
# 如果是 SOFABoot,则:
+# 配置健康检查跳过 JVM 服务检查
+com.alipay.sofa.boot.skip-jvm-reference-health-check=true
+# 忽略未解析的占位符
+com.alipay.sofa.ignore.unresolvable.placeholders=true
+

开启方式

+

模块打包插件里增加如下配置:
image.png

+

开启后的副作用

+

如果模块委托给基座的依赖里有发布服务,那么基座和模块会同时发布两份。

+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 27, 2023: fix doc (285936d5) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/introduction/architecture/index.html b/docs/public/docs/introduction/architecture/index.html index b5dfa6821..06754624b 100644 --- a/docs/public/docs/introduction/architecture/index.html +++ b/docs/public/docs/introduction/architecture/index.html @@ -1,12 +1,502 @@ -架构介绍 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +架构介绍 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

架构介绍

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

架构介绍

+ + + +
+ + + + + + + + +
+ + +
+
+ 架构原理 +
+

+
+ + +
+
+ 基座与模块间类委托加载原理介绍 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/introduction/index.html b/docs/public/docs/introduction/index.html index 71c2f3bfb..e7826f3c2 100644 --- a/docs/public/docs/introduction/index.html +++ b/docs/public/docs/introduction/index.html @@ -1,12 +1,510 @@ -产品介绍 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +产品介绍 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

产品介绍

-

最后修改 September 22, 2023: add docs (efcf6b56)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

产品介绍

+ + + +
+ + + + + + + + +
+ + +
+
+ 简介与适用场景 +
+

+
+ + +
+
+ 行业背景 +
+

+
+ + +
+
+ 架构介绍 +
+

+
+ + +
+ +
+ + + + + + +
+ +
+
+ 最后修改 September 22, 2023: add docs (efcf6b56) +
+
+ +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/introduction/industry-background/index.html b/docs/public/docs/introduction/industry-background/index.html index 8cd88ec3c..dbf44cdf5 100644 --- a/docs/public/docs/introduction/industry-background/index.html +++ b/docs/public/docs/introduction/industry-background/index.html @@ -1,4 +1,25 @@ -行业背景 | SOFAServerless + + + + + + + + + + + + + + + + + + +行业背景 | SOFAServerless + + + + + + + + + + + + + + - - +从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分 只解决其中部分问题 存量应用接入改造成本高 SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。"/> + + + + + + + + + + + + + + + + + + + -

行业背景

微服务的问题

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

基础设施复杂

认知负载高

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

运维负担重

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

  1. 重要功能版本发布
  2. 修复紧急 bug
  3. 遇到重大安全漏洞

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

对于业务研发人员

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

对于基础设施人员

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

启动慢

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

发布效率低

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

协作与资源成本高

单体应用/大应用过大

image.png

多人协作阻塞

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

变更影响面大,风险高

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

小应用过多

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

资源成本高

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

长期维护成本

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

问题必然性

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

如何合理拆分微服务

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

微服务的拆分与业务和组织发展敏捷度不一致

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

不同体量企业面对的问题

image.png

行业尝试的解法

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. 只解决其中部分问题
  3. 存量应用接入改造成本高

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。



-

最后修改 October 30, 2023: update home (f751b32b)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

行业背景

+ + +

微服务的问题

+

应用架构从单体应用发展到微服务,结合软件工程从瀑布模式到当前的 DevOps 模式的发展,解决了可扩展、分布式、分工协作等问题,为企业提供较好的敏捷性与执行效率,带来了明显的价值。但该模式发展至今,虽然解决了一些问题,也有微服务的一些问题慢慢暴露出来,在当前已经得到持续关注:

+

基础设施复杂

+

认知负载高

+

image.png
当前业务要完成一个需求,背后实际上有非常多的依赖、组件和平台在提供各种各样的能力,只要这些业务以下的某一个组件出现异常被业务感知到,都会对业务研发人员带来较大认知负担和对应恢复的时间成本。
image.png
异常种类繁多

+

运维负担重

+

业务包含的各个依赖也会不断迭代升级,例如框架、中间件、各种 sdk 等,在遇到

+
    +
  1. 重要功能版本发布
  2. +
  3. 修复紧急 bug
  4. +
  5. 遇到重大安全漏洞
  6. +
+

等情况时,这些依赖的新版本就需要业务尽可能快的完成升级,这造成了两方面的问题:

+
对于业务研发人员
+

这些依赖的升级如果只是一次两次那么就不算是问题,但是一个业务应用背后依赖的框架、中间件与各类 sdk 是很多的,每一个依赖发布这些升级都需要业务同学来操作,这么多个依赖的话长期上就会对业务研发同学来说是不小的运维负担。另外这里也需要注意到业务公共层对业务开发者来说也是不小的负担。

+
对于基础设施人员
+

类似的对于各个依赖的开发人员自身,每发布一个这样的新版本,需要尽可能快的让使用的业务应用完成升级。但是业务研发人员更关注业务需求交付,想要推动业务研发人员快速完成升级是不太现实的,特别是在研发人员较多的企业里。

+

启动慢

+

每个业务应用启动过程都需要涉及较多过程,造成一个功能验证需要花费较长等待时间。
image.png

+

发布效率低

+

由于上面提到的启动慢、异常多的问题,在发布上线过程中需要较长时间,出现异常导致卡单需要恢复处理。发布过程中除了平台异常外,机器异常发生的概率会随着机器数量的增多而增多,假如一台机器正常完成发布(不发生异常)的概率是 99.9%,也就是一次性成功率为 99.9%,那么100台则是 90%,1000台则降低到了只有 36.7%,所以对于机器较多的应用发布上线会经常遇到卡单的问题,这些都需要研发人员介入处理,导致效率低。

+

协作与资源成本高

+

单体应用/大应用过大

+

image.png

+
多人协作阻塞
+

业务不断发展,应用会不断变大,这主要体现在开发人员不断增多,出现多人协作阻塞问题。

+
变更影响面大,风险高
+

业务不断发展,线上流量不断增大,机器数量也不断增多,但当前一个变更可能影响全部代码和机器流量,变更影响面大风险高。

+

小应用过多

+

image.png
在微服务发展过程中,随着时间的推移,例如部分应用拆分过多、某些业务萎缩、组织架构调整等,都会出现线上小应用或者长尾应用不断积累,数量越来越多,像蚂蚁过去3年应用数量增长了 3倍。
image.png

+
资源成本高
+

这些应用每个机房都需要几台机器,但其实流量也不大,cpu 使用率很低,造成资源浪费。

+
长期维护成本
+

这些应用同样需要人员来维度,例如升级 SDK,修复安全漏洞等,长期维护成本高。

+

问题必然性

+

微服务系统是个生态,在一个公司内发展演进几年后,参考28定律,少数的大应用占有大量的流量,不可避免的会出现大应用过大和小应用过多的问题。
然而大应用多大算大,小应用多少算多,这没有定义的标准,所以这类问题造成的研发人员的痛点是隐匿的,没有痛到一定程度是较难引起公司管理层面的关注和行动。

+

如何合理拆分微服务

+

微服务如何合理拆分始终是个老大难的问题,合理拆分始终没有清晰的标准,这也是为何会存在上述的大应用过大、小应用过多问题的原因。而这些问题背后的根因是业务与组织灵活,与微服务拆分的成本高,两者的敏捷度不一致。

+

微服务的拆分与业务和组织发展敏捷度不一致

+

image.png
业务发展灵活,组织架构也在不断调整,而微服务拆分需要机器与长期维护的成本,两者的敏捷度不一致,导致容易出现未拆或过度拆分问题,从而出现大应用过大和小应用过多问题。这类问题不从根本上解决,会导致微服务应用治理过一波之后还会再次出现问题,导致研发同学始终处于低效的痛苦与治理的痛苦循环中。

+

不同体量企业面对的问题

+

image.png

+

行业尝试的解法

+

当前行业里也有很多不错的思路和项目在尝试解决这些问题,例如服务网格、应用运行时、平台工程,Spring Modulith、Google ServiceWeaver,有一定的效果,但也存在一定的局限性:

+
    +
  1. 从业务研发人员角度看,只屏蔽部分基础设施,未屏蔽业务公共部分
  2. +
  3. 只解决其中部分问题
  4. +
  5. 存量应用接入改造成本高
  6. +
+

SOFAServerless 的目的是为了解决这些问题而不断演进出来的一套研发框架与平台能力。

+
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 October 30, 2023: update home (f751b32b) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/introduction/intro-and-scenario/index.html b/docs/public/docs/introduction/intro-and-scenario/index.html index ad6c1b892..e5d421f75 100644 --- a/docs/public/docs/introduction/intro-and-scenario/index.html +++ b/docs/public/docs/introduction/intro-and-scenario/index.html @@ -1,36 +1,538 @@ -简介与适用场景 | SOFAServerless + + + + + + + + + + + + + + + + + + +简介与适用场景 | SOFAServerless + + + + + + + + + + + + + + - - +这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。"/> + + + + + + + + + + + + + + + + + + + -

简介与适用场景

简介

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

适用场景

痛点 1:应用构建发布慢或者 SDK 升级繁琐

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

痛点 2:长尾应用资源成本高

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本

这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

痛点 3:企业研发协作效率低

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。

痛点 4:难以沉淀业务资产提高中台效率

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。

痛点 5:微服务演进成本高

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进



-

最后修改 November 2, 2023: udpate doc (43cd10f1)
- - \ No newline at end of file + + + +
+ +
+
+
+
+ + +
+ + + + + +
+

简介与适用场景

+ + +

简介

+

SOFAServerless 是一种模块化的 Serverless 技术解决方案,它能让普通应用以比较低的代价演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护,与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。
随着各行各业的信息化数字化转型,企业面临越来越多的研发效率、协作效率、资源成本和服务治理痛点,接下来带领大家逐一体验这些痛点,以及它们在 SOFAServerless 中是如何被解决的。

+

适用场景

+

痛点 1:应用构建发布慢或者 SDK 升级繁琐

+

传统应用镜像化构建一般要 3 - 5 分钟,单机代码发布到启动完成也要 3 - 5 分钟,开发者每次验证代码修改或上线代码修改,都需要经历数次 6 - 10 分钟的构建发布等待,严重影响开发效率。此外,每次 SDK 升级(比如中间件框架、rpc、logging、json 等),都需要修改所有应用代码并重新构建发布,对开发者也造成了不必要的打扰。
通过使用 SOFAServerless 通用基座与配套工具,您可以低成本的将应用切分为 “基座” 与 “模块”,其中基座沉淀了公司或者某个业务部门的公共 SDK,基座升级可以由专人负责,对业务开发者无感,业务开发者只需要编写模块。在我们目前支持的 Java 技术栈中,模块就是一个 SpringBoot 应用代码包(FatJar),只不过 SpringBoot 框架本身和其他的企业公共依赖在运行时会让基座提前加载预热,模块每次发布都会找一台预热 SpringBoot 的基座进行热部署,整个过程类似 AppEngine,能够帮用户实现应用 10 秒级构建发布公共 SDK 升级无感

+ +

痛点 2:长尾应用资源成本高

+

在企业中,80% 的应用只服务了不到 20% 的流量,同时伴随着业务的变化,企业存在大量的长尾应用,这些长尾应用 CPU 使用率长期不到 10%,造成了极大的资源浪费
通过使用 SOFAServerless 合并部署与配套工具,您可以低成本的实现多个应用的合并部署,从而解决企业应用过度拆分和低流量业务带来的资源浪费节约成本
+
+这里 “业务A 应用1” 在 SOFAServerless 术语中叫 “模块”。多个模块应用可以使用 SOFAArk 技术合并到同一个基座。基座可以是完全空的 SpringBoot应用(Java 技术栈),也可以下沉一些公共 SDK 到基座,模块应用每次发布会重启基座机器。在这种方式下,模块应用最大程度复用了基座的内存(Metaspace 和 Heap),构建产物大小也能从数百 MB 瘦身到几十 MB 甚至更激进,CPU 使用率也得到了有效提升。

+

痛点 3:企业研发协作效率低

+

在企业中,一些应用需要多人开发协作。在传统研发模式下,每一个人的代码变更都需要发布整个应用,这就导致应用需要以赶火车式的方式进行研发迭代,大家需要统一一个时间窗口做迭代开发,统一的时间点做发布上线,因此存在大量的需求上线相互等待、环境机器抢占、迭代冲突等情况。
通过使用 SOFAServerless,您可以方便的将应用拆分为一个基座与多个功能模块,一个功能模块就是一组代码文件。不同的功能模块可以同时进行迭代开发和发布运维,模块间互不感知互不影响,这样就消除了传统应用迭代赶火车式的相互等待,每个模块拥有自己的独立迭代,需求交付效率因此得到了极大提升。如果您对模块额外启用了热部署方式(也可以每次发布模块重启整个基座),那么模块的单次构建+发布也会从普通应用的 6 - 10 分钟减少到十秒级。
+

+

痛点 4:难以沉淀业务资产提高中台效率

+

在一些中大型企业中,会沉淀各种业务中台应用。中台一般封装了业务的公共 API 实现,和 SPI 定义。其中 SPI 定义允许中台上的插件去实现各自的业务逻辑,流量进入中台应用后,会调用对应的 SPI 实现组件去完成相应的业务逻辑。中台应用内的组件,业务逻辑一般不复杂,如果拆出去部署为独立应用会带来高昂的资源成本和运维成本,而且构建发布速度很慢,严重加剧研发负担影响研发效率。
通过使用 SOFAServerless,您可以方便的将中台应用拆分一个基座和多个功能模块。基座可以沉淀比较厚的业务依赖、公共逻辑、API 实现、SPI 定义等(即所谓的业务资产),提供给上面的模块使用。模块使用基座的能力可以是对象之间或 Bean 之间的直接调用,代码几乎不用改造。而且多个模块间可以同时进行迭代开发和发布运维,互不感知互不影响协作交付效率得到了极大提升。此外对于比较简单的模块还可以开启热部署,单次构建+发布也会从普通应用的 6 - 10 分钟减少到 30 秒内。
+

+

痛点 5:微服务演进成本高

+

企业里不同业务有不同的发展阶段,因此应用也拥有自己的生命周期。

+

初创期:一个初创的应用一般会先采用单体架构

增长期:随着业务增长,应用开发者也随之增加。此时您可能不确定业务的未来前景,也不希望过早把业务拆分成多个应用以避免不必要的维护、治理和资源成本,那么您可以用 SOFAServerless 低成本地将应用拆分为一个基座和多个功能模块,不同功能模块之间可以并行研发运维独立迭代,从而提高应用在此阶段的研发协作和需求交付效率。

成熟期:随着业务进一步增长,您可以使用 SOFAServerless 低成本地将部分或全部功能模块拆分成独立应用去研发运维。

长尾期:部分业务在经历增长期或者成熟期后,也可能慢慢步入到低活状态或者长尾状态,此时您可以用 SOFAServerless 低成本地将这些应用一键改回模块合并部署到一起实现降本增效

+

可以看到 SOFAServerless 支持企业应用低成本地在初创期、增长期、成熟期、长尾期之间平滑过渡甚至来回切换,从而轻松让应用架构与业务发展保持同步。
应用生命周期演进
+

+
+
+ + +
+ + + + + + +
+ + +
+
+ 最后修改 November 2, 2023: udpate doc (43cd10f1) +
+ +
+ + +
+
+
+
+
+
+
+ + + + + + + +
+
+ + + + + + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/docs/public/docs/quick-start/_print/index.html b/docs/public/docs/quick-start/_print/index.html index b64431453..0ca37cfff 100644 --- a/docs/public/docs/quick-start/_print/index.html +++ b/docs/public/docs/quick-start/_print/index.html @@ -1,9 +1,256 @@ -快速开始 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +快速开始 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

这是本节的多页打印视图。 -点击此处打印.

返回本页常规视图.

快速开始

    实验 1:一键实现多应用合并部署

    合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

    1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
    2. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署
      您也可以直接使用 官方 Demo 和文档 在本地完成实验。

    小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。



    实验 2:一键体验应用秒级热部署

    步骤 1:本地软件安装

    下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

    步骤 2:一键启动 SOFAServerless

    使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
    module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

    步骤 3:秒级发布模块

    执行以下命令:

    kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml
    -

    即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
    image.png

    步骤 4:清理本地环境

    您可以使用 make undev 删除所有本地资源,清理本地环境。



    欢迎大家学习 SOFAServerless 视频教程

    点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    +
    + + + + + +
    +
    +

    +这是本节的多页打印视图。 +点击此处打印. +

    +返回本页常规视图. +

    +
    + + + +

    快速开始

    + + + + + +
      + + + + + + + + +
    + + +
    +

    实验 1:一键实现多应用合并部署

    +

    合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

    +
      +
    1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
    2. +
    3. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署。 +
      +您也可以直接使用 官方 Demo 和文档 在本地完成实验。
    4. +
    +

    小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。

    +
    +
    +

    实验 2:一键体验应用秒级热部署

    +

    步骤 1:本地软件安装

    +

    下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

    +

    步骤 2:一键启动 SOFAServerless

    +

    使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
    module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

    +

    步骤 3:秒级发布模块

    +

    执行以下命令:

    +
    kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml
    +

    即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
    image.png

    +

    步骤 4:清理本地环境

    +

    您可以使用 make undev 删除所有本地资源,清理本地环境。

    +
    +
    +

    欢迎大家学习 SOFAServerless 视频教程

    +

    点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

    + +
    +
    + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + diff --git a/docs/public/docs/quick-start/index.html b/docs/public/docs/quick-start/index.html index 17bf32b56..5a35e4020 100644 --- a/docs/public/docs/quick-start/index.html +++ b/docs/public/docs/quick-start/index.html @@ -1,13 +1,520 @@ -快速开始 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +快速开始 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    快速开始

    实验 1:一键实现多应用合并部署

    合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

    1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
    2. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署
      您也可以直接使用 官方 Demo 和文档 在本地完成实验。

    小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。



    实验 2:一键体验应用秒级热部署

    步骤 1:本地软件安装

    下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

    步骤 2:一键启动 SOFAServerless

    使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
    module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

    步骤 3:秒级发布模块

    执行以下命令:

    kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml
    -

    即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
    image.png

    步骤 4:清理本地环境

    您可以使用 make undev 删除所有本地资源,清理本地环境。



    欢迎大家学习 SOFAServerless 视频教程

    点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

    -

    最后修改 November 8, 2023: update docs (8ef3ef71)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    快速开始

    + + +

    实验 1:一键实现多应用合并部署

    +

    合并部署是指:选定一个应用作为底座,然后将多个其它应用合并部署到这个底座之上,从而实现长尾应用的极致资源降本。典型业务场景为应用的低成本交付 以及 微服务过度拆分一键重新合并。

    +
      +
    1. 选定一个应用作为底座(SOFAServerless 术语叫基座),将普通应用一键升级为基座
    2. +
    3. 选定一个应用作为上层应用(SOFAServerless 术语叫模块),将其一键转为模块应用并完成合并部署。 +
      +您也可以直接使用 官方 Demo 和文档 在本地完成实验。
    4. +
    +

    小贴士:无论基座还是模块,接入 SOFAServerless 后,同一套代码分支既能像原来一样独立启动,又能做到合并部署。

    +
    +
    +

    实验 2:一键体验应用秒级热部署

    +

    步骤 1:本地软件安装

    +

    下载安装 go(建议 1.20 或以上)、dockerminikubekubectl

    +

    步骤 2:一键启动 SOFAServerless

    +

    使用 git 拉取 GitHub sofa-severless 项目:https://github.com/sofastack/sofa-serverless
    module-controller 目录下执行 make dev 命令一键部署环境,会自动执行 minikube service 命令弹出网页,由于此时您还没有发布模块,所以网页不会有任何内容显示。

    +

    步骤 3:秒级发布模块

    +

    执行以下命令:

    +
    kubectl apply -f config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml
    +

    即可秒级发布上线模块应用。请等待本地 Module CR 资源 Status 字段值变更为 Available**(约 1 秒,表示模块发布完毕)**,再刷新步骤 2 自动打开的网页,即可看到一个简单的卖书页面,这个卖书逻辑就是在模块里实现的:
    image.png

    +

    步骤 4:清理本地环境

    +

    您可以使用 make undev 删除所有本地资源,清理本地环境。

    +
    +
    +

    欢迎大家学习 SOFAServerless 视频教程

    +

    点击此处查看 SOFAServerless 平台与研发框架视频培训教程。

    + +
    + + + + + + + + + +
    + +
    + + + + + + +
    + +
    +
    + 最后修改 November 8, 2023: update docs (8ef3ef71) +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/_print/index.html b/docs/public/docs/tutorials/_print/index.html index 540f4d128..c4aa0a028 100644 --- a/docs/public/docs/tutorials/_print/index.html +++ b/docs/public/docs/tutorials/_print/index.html @@ -1,817 +1,2474 @@ -用户手册 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +用户手册 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    1 - 基座接入

    1.1 - SpringBoot 或 SOFABoot 升级为基座

    前提条件

    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

    接入步骤

    代码与配置修改

    修改 application.properties

    # 需要定义应用名
    -spring.application.name = ${替换为实际基座应用名}
    -

    修改主 pom.xml

    <properties>
    -    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    -    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    -</properties>
    -
    <dependency>
    -    <groupId>com.alipay.sofa.serverless</groupId>
    -    <artifactId>sofa-serverless-base-starter</artifactId>
    -    <version>${sofa.serverless.runtime.version}</version>
    -</dependency>
    -
    -<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
    -<dependency>
    -    <groupId>com.alipay.sofa</groupId>
    -    <artifactId>web-ark-plugin</artifactId>
    -</dependency>
    -

    启动验证

    基座应用能正常启动即表示验证成功!



    2 - 模块接入

    2.1 - SpringBoot 或 SOFABoot 一键升级为模块

    本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

    前提条件

    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

    接入步骤

    步骤 1:修改 application.properties

    # 需要定义应用名
    -spring.application.name = ${替换为实际模块应用名}
    -

    步骤 2:添加模块需要的依赖和打包插件

    特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

    
    -<plugins>
    -    <!--这里添加ark 打包插件-->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>{sofa.ark.version}</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>${替换为模块名}</bizName>
    -            <webContextPath>${模块自定义的 web context path}</webContextPath>
    -            <declaredMode>true</declaredMode>
    -        </configuration>
    -    </plugin>
    -    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    -    <plugin>
    -        <!--原来 spring-boot 打包插件 -->
    -        <groupId>org.springframework.boot</groupId>
    -        <artifactId>spring-boot-maven-plugin</artifactId>
    -    </plugin>
    -</plugins>
    -

    步骤 3:自动化瘦身模块

    您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 -扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

    步骤 4:构建成模块 jar 包

    执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

    小贴士模块中支持的完整中间件清单

    实验:验证模块既能独立启动,也能被合并部署

    增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 -当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

    验证能合并部署到基座上

    1. 启动上一步(验证能独立启动步骤)的基座
    2. 发起模块部署
    curl --location --request POST 'localhost:1238/installBiz' \
    ---header 'Content-Type: application/json' \
    ---data '{
    -    "bizName": "${模块名}",
    -    "bizVersion": "${模块版本}",
    -    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
    -}'
    -

    返回如下信息表示模块安装成功
    image.png

    1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider

    image.png

    1. 卸载模块
    curl --location --request POST 'localhost:1238/uninstallBiz' \
    ---header 'Content-Type: application/json' \
    ---data '{
    -    "bizName": "dynamic-provider",
    -    "bizVersion": "0.0.1-SNAPSHOT"
    -}'
    -

    返回如下,表示卸载成功

    {
    -    "code": "SUCCESS",
    -    "data": {
    -        "code": "SUCCESS",
    -        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    -    }
    -}
    -

    验证能独立启动

    普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
    image.png

    2.2 - 使用 maven archtype 脚手架自动生成

    正在更新中,预计 11 月上线。

    3 - 基座与模块并行开发验证

    欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 -首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

    1. 合并部署
    2. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)

    本文实验工程代码在:开源仓库 samples 目录库里

    场景一:合并部署

    先介绍第一个场景多应用合并部署,整体流程如下: -image.png

    可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

    1. 基座接入改造

    1. 为 **application.properties **增加应用名(如果没有的话):

    spring.application.name=${基座应用名}

    1. 在 **pom.xml **里增加必要的依赖
    <properties>
    -    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    -</properties>
    -<dependencies>
    -    <dependency>
    -        <groupId>com.alipay.sofa.serverless</groupId>
    -        <artifactId>sofa-serverless-base-starter</artifactId>
    -        <version>${sofa.serverless.runtime.version}</version>
    -    </dependency>
    -</dependencies>
    -

    理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

        <dependency>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>web-ark-plugin</artifactId>
    -    </dependency>
    -
    1. 点击编译器启动基座。

    2. 模块 1 接入改造

    1. 添加模块需要的依赖和打包插件
    <plugins>
    -    <!--这里添加ark 打包插件-->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>${替换为模块名}</bizName>
    -            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
    -            <declaredMode>true</declaredMode>
    -            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
    -            <packExcludesConfig>rules.txt</packExcludesConfig>
    -        </configuration>
    -    </plugin>
    -    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    -    <plugin>
    -        <!--原来 spring-boot 打包插件 -->
    -        <groupId>org.springframework.boot</groupId>
    -        <artifactId>spring-boot-maven-plugin</artifactId>
    -    </plugin>
    -</plugins>
    -
    1. 参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

    2. 开发模块,例如增加 Rest Controller,提供 Rest 接口

    @RestController
    -public class SampleController {
    -    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    -
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -        String appName = applicationContext.getApplicationName();
    -        LOGGER.info("{} web test: into sample controller", appName);
    -        return String.format("hello to %s deploy", appName);
    -    }
    -}
    -
    1. 点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

    2. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

    hello to ${模块1名} deploy
    -

    3. 模块 1 开发与验证

    开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

    1. 修改 Rest 代码
    @RestController
    -public class SampleController {
    -    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    -
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -        String appName = applicationContext.getApplicationName();
    -        LOGGER.info("{} web test v2: into sample controller", appName);
    -        return String.format("hello to %s deploy v2", appName);
    -    }
    -}
    -
    1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
    hello to ${模块1名} deploy v2
    -

    4. 模块 2 接入改造、开发与验证

    模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

    场景二:中台应用

    中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: -image.png

    可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI。 -接下来我们将介绍与合并部署不同的(即新增的)操作细节。

    1. 基座完成通信类和 SPI 的定义

    在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 -通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

    1. 新建 bundle,定义接口类
    public class ProductInfo {
    -    private String  name;
    -    private String  author;
    -    private String  src;
    -    private Integer orderCount;
    -}
    -
    1. 定义 SPI
    public interface StrategyService {
    -    List<ProductInfo> strategy(List<ProductInfo> products);
    -    String getAppName();
    -}
    -

    2. 模块 1 引入通信类基座并实现基座 SPI

    在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

    1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
    2. 定义 SPI 实现
    @Service
    -public class StrategyServiceImpl implements StrategyService {
    -
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @Override
    -    public List<ProductInfo> strategy(List<ProductInfo> products) {
    -        return products;
    -    }
    -
    -    @Override
    -    public String getAppName() {
    -        return applicationContext.getApplicationName();
    -    }
    -}
    -
    1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回

    biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

    默认的 products 列表
    -

    3. 模块 2 引入通信类基座并实现基座 SPI

    与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

    @Service
    -public class StrategyServiceImpl implements StrategyService {
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @Override
    -    public List<ProductInfo> strategy(List<ProductInfo> products) {
    -        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
    -        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
    -        return products;
    -    }
    -
    -    @Override
    -    public String getAppName() {
    -        return applicationContext.getApplicationName();
    -    }
    -}
    -
    更改排序后的 products 列表
    -

    基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

    文档中的链接地址

    1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
    2. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
    3. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
    4. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
    5. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/

    4 - 模块研发

    4.1 - 编码规范

    基础规范

    1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
    2. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
    3. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布

    知识点

    模块瘦身 (重要)
    模块与模块、模块与基座通信 (重要)
    模块测试 (重要)
    模块复用基座拦截器
    模块复用基座数据源
    基座与模块间类委托加载原理介绍



    4.2 - 模块瘦身

    为什么要瘦身

    为了让模块安装更快、内存消耗更小:

    • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
    • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。

    一键自动瘦身

    瘦身原则

    构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

    步骤一

    在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

    excludeGroupIds=org.apache*
    -excludeArtifactIds=commons-lang
    -

    步骤二

    在模块打包插件中,引入上述配置文件:

        <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>2.2.5</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>biz1</bizName>
    -            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
    -            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
    -            <packExcludesConfig>rules.txt</packExcludesConfig>
    -            <webContextPath>biz1</webContextPath>
    -            <declaredMode>true</declaredMode>
    -            <!--					打包、安装和发布 ark biz-->
    -            <!--					静态合并部署需要配置-->
    -            <!--					<attach>true</attach>-->
    -        </configuration>
    -    </plugin>
    -

    步骤三

    打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

    您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

    基本原理

    SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

    1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
    2. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。

    基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
    所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

    手动排包瘦身

    模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
    所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
    image.png

    如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
    image.png
    确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
    image.png

    在 pom 中统一排包(更彻底)

    有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

    <dependency>
    -    <groupId>com.serverless.mymodule</groupId>
    -    <artifactId>mymodule-core</artifactId>
    -    <exclusions>
    -          <exclusion>
    -              <groupId>org.springframework</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -          <exclusion>
    -              <groupId>org.apache.commons</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -          <exclusion>
    -              <groupId>......</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -    </exclusions>
    -</dependency>
    -

    在 sofa-ark-maven-plugin 中指定排包

    通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

     <plugin>
    -      <groupId>com.alipay.sofa</groupId>
    -      <artifactId>sofa-ark-maven-plugin</artifactId>
    -      <executions>
    -          <execution>
    -              <id>default-cli</id>
    -              <goals>
    -                  <goal>repackage</goal>
    -              </goals>
    -          </execution>
    -      </executions>
    -      <configuration>
    -          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
    -          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
    -          <outputDirectory>../../target</outputDirectory>
    -          <bizName>mymodule</bizName>
    -          <finalName>mymodule-${project.version}-${timestamp}</finalName>
    -          <bizVersion>${project.version}-${timestamp}</bizVersion>
    -          <webContextPath>/mymodule</webContextPath>
    -      </configuration>
    -  </plugin>
    -


    4.3 - 模块与模块、模块与基座通信

    基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 -当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

    Spring 环境

    基座调用模块

    只能使用 SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    -                "studentProvider", Provider.class);
    -        Result result = studentProvider.provide(new Param());
    -
    -        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    -                "teacherProvider", Provider.class);
    -        Result result1 = teacherProvider.provide(new Param());
    -        
    -        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
    -                Provider.class);
    -        for (String beanName : providerMap.keySet()) {
    -            Result result2 = providerMap.get(beanName).provide(new Param());
    -        }
    -
    -        return "hello to ark master biz";
    -    }
    -}
    -

    模块调用基座

    方式一:注解 @AutowiredFromBase

    @RestController
    -public class SampleController {
    -
    -    @AutowiredFromBase(name = "sampleServiceImplNew")
    -    private SampleService sampleServiceImplNew;
    -
    -    @AutowiredFromBase(name = "sampleServiceImpl")
    -    private SampleService sampleServiceImpl;
    -
    -    @AutowiredFromBase
    -    private List<SampleService> sampleServiceList;
    -
    -    @AutowiredFromBase
    -    private Map<String, SampleService> sampleServiceMap;
    -
    -    @AutowiredFromBase
    -    private AppService appService;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        sampleServiceImplNew.service();
    -
    -        sampleServiceImpl.service();
    -
    -        for (SampleService sampleService : sampleServiceList) {
    -            sampleService.service();
    -        }
    -
    -        for (String beanName : sampleServiceMap.keySet()) {
    -            sampleServiceMap.get(beanName).service();
    -        }
    -
    -        appService.getAppName();
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    方式二:编程API SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
    -        String result = sampleServiceImplFromFinder.service();
    -        System.out.println(result);
    -
    -        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
    -        for (String beanName : sampleServiceMapFromFinder.keySet()) {
    -            String result1 = sampleServiceMapFromFinder.get(beanName).service();
    -            System.out.println(result1);
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    模块调用模块

    参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

    方式一:注解 @AutowiredFromBiz

    @RestController
    -public class SampleController {
    -
    -    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    -    private Provider studentProvider;
    -
    -    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    -    private Provider teacherProvider;
    -
    -    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    -    private List<Provider> providers;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Result provide = studentProvider.provide(new Param());
    -
    -        Result provide1 = teacherProvider.provide(new Param());
    -
    -        for (Provider provider : providers) {
    -            Result provide2 = provider.provide(new Param());
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    方式二:编程API SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
    -        Result result1 = teacherProvider1.provide(new Param());
    -
    -        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
    -        for (String beanName : providerMap.keySet()) {
    -            Result result2 = providerMap.get(beanName).provide(new Param());
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    完整样例

    SOFABoot 环境

    请参考该文档



    4.4 - 模式本地开发

    Arkctl 工具安装

    方法一:

    1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
    2. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。

    方法二:

    1. 二进制列表 中下载对应的二进制并加入到本地 -path 中。

    本地快速部署

    你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

    场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

    准备:

    1. 在本地启动一个基座。
    2. 打开一个模块项目仓库。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

    准备:

    1. 在本地启动一个基座。
    2. 准备一个构建好的 jar 包。

    执行命令:

    arkctl deploy /path/to/your/pre/built/bundle-biz.jar
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

    准备:

    1. 在远程已经运行起来的基座 pod。
    2. 打开一个模块项目仓库。
    3. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy --pod {namespace}/{podName}
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

    准备:

    1. 在本地启动一个基座。
    2. 打开一个多模块 Maven 项目仓库。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy --sub ./path/to/your/sub/module
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 5: 查询当前基座中已经部署的模块。

    准备:

    1. 在本地启动一个基座。

    执行命令:

    arkctl status
    -

    场景 6: 查询远程 k8s 环境基座中已经部署的模块。

    准备:

    1. 在远程 k8s 环境启动一个基座。
    2. 确保本地有 kube 证书以及有关权限。

    执行命令:

    arkctl status --pod {namespace}/{name}
    -

    4.5 - 模式测试

    本地调试

    您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
    Arklet HTTP 接口主要提供了以下能力:

    1. 部署和卸载模块。
    2. 查询所有已部署的模块信息。
    3. 查询各项系统和业务指标。

    部署模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
    -

    请求体样例:

    {
    -    "bizName": "test",
    -    "bizVersion": "1.0.0",
    -    // local path should start with file://, alse support remote url which can be downloaded
    -    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
    -}
    -

    部署成功返回结果样例:

    {
    -  "code":"SUCCESS",
    -  "data":{
    -    "bizInfos":[
    -      {
    -        "bizName":"dynamic-provider",
    -        "bizState":"ACTIVATED",
    -        "bizVersion":"1.0.0",
    -        "declaredMode":true,
    -        "identity":"dynamic-provider:1.0.0",
    -        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    -        "priority":100,
    -        "webContextPath":"provider"
    -      }
    -    ],
    -    "code":"SUCCESS",
    -    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
    -  }
    -}
    -

    部署失败返回结果样例:

    {
    -  "code":"FAILED",
    -  "data":{
    -    "code":"REPEAT_BIZ",
    -    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
    -  }
    -}
    -

    卸载模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
    -

    请求体样例:

    {
    -    "bizName":"dynamic-provider",
    -    "bizVersion":"1.0.0"
    -}
    -

    卸载成功返回结果样例:

    {
    -  "code":"SUCCESS"
    -}
    -

    卸载失败返回结果样例:

    {
    -  "code":"FAILED",
    -  "data":{
    -    "code":"NOT_FOUND_BIZ",
    -    "message":"Uninstall biz: test:1.0.0 not found."
    -  }
    -}
    -

    查询模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
    -

    请求体样例:

    {}
    -

    返回结果样例:

    {
    -  "code":"SUCCESS",
    -  "data":[
    -    {
    -      "bizName":"dynamic-provider",
    -      "bizState":"ACTIVATED",
    -      "bizVersion":"1.0.0",
    -      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    -      "webContextPath":"provider"
    -    },
    -    {
    -      "bizName":"stock-mng",
    -      "bizState":"ACTIVATED",
    -      "bizVersion":"1.0.0",
    -      "mainClass":"embed main",
    -      "webContextPath":"/"
    -    }
    -  ]
    -}
    -

    获取帮助

    Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
    -

    请求体样例:

    {}
    -

    返回结果样例:

    {
    -    "code":"SUCCESS",
    -    "data":[
    -        {
    -            "desc":"query all ark biz(including master biz)",
    -            "id":"queryAllBiz"
    -        },
    -        {
    -            "desc":"list all supported commands",
    -            "id":"help"
    -        },
    -        {
    -            "desc":"uninstall one ark biz",
    -            "id":"uninstallBiz"
    -        },
    -        {
    -            "desc":"switch one ark biz",
    -            "id":"switchBiz"
    -        },
    -        {
    -            "desc":"install one ark biz",
    -            "id":"installBiz"
    -        }
    -    ]
    -}
    -

    本地构建如何不改变模块版本号

    添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

    <profile>
    -    <id>local</id>
    -    <build>
    -        <plugins>
    -            <plugin>
    -                <groupId>com.alipay.sofa</groupId>
    -                <artifactId>sofa-ark-maven-plugin</artifactId>
    -                <configuration>
    -                    <finalName>${project.artifactId}-${project.version}</finalName>
    -                    <bizVersion>${project.version}</bizVersion>
    -                </configuration>
    -            </plugin>
    -        </plugins>
    -    </build>
    -</profile>
    -

    单元测试

    模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。



    4.6 - 复用基座拦截器

    诉求

    基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

    解法

    为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

    步骤 1:

    基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

    public interface AnnotionService {
    -    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
    -}
    -

    步骤 2:

    在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

    @Service
    -@SofaService(uniqueId = "facadeAroundHandler")
    -public class FacadeAroundHandler implements AnnotionService {
    -
    -    private final static Logger LOG = LoggerConst.MY_LOGGER;
    -
    -    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    -        log.info("开始执行")
    -        joinPoint.proceed();
    -        log.info("执行完成")
    -    }
    -}
    -

    步骤 3:

    在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
    注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

    //注意,这里不必申明成一个bean,不要加@Component或者@Service
    -@Aspect
    -public class FacadeAroundAspect {
    -
    -    @SofaReference(uniqueId = "facadeAroundHandler")
    -    private AnnotionService facadeAroundHandler;
    -
    -    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    -    public void facadeAroundPointcut() {
    -    }
    -
    -    @Around("facadeAroundPointcut()")
    -    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    -        return facadeAroundHandler.doAround(joinPoint);
    -    }
    -}
    -

    步骤 4:

    使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
    注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

    @Configuration
    -public class MngAspectConfiguration {
    -    @Bean
    -    public FacadeAroundAspect facadeAroundAspect() {
    -        return new FacadeAroundAspect();
    -    }
    -    @Bean
    -    public EnvRouteAspect envRouteAspect() {
    -        return new EnvRouteAspect();
    -    }
    -    @Bean
    -    public FacadeAroundAspect facadeAroundAspect() {
    -        return new FacadeAroundAspect();
    -    }
    -    
    -}
    -

    步骤 5:

    模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

    @SpringBootApplication
    -@ImportResource("classpath*:META-INF/spring/*.xml")
    -@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
    -public class ModuleBootstrapApplication {
    -    public static void main(String[] args) {
    -        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
    -        	.web(WebApplicationType.NONE);
    -        builder.build().run(args);
    -    }
    -}
    -


    4.7 - 复用基座数据源

    建议

    强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

    SpringBoot 解法

    在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
    通过BaseAppUtils.getBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

    
    -@Configuration
    -@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    -@EnableTransactionManagement
    -public class MybatisConfig {
    -
    -    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    -    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    -
    -    @Bean(name = "transactionManager")
    -    public PlatformTransactionManager platformTransactionManager() {
    -        return (PlatformTransactionManager) BaseAppUtils.getBean("transactionManager");
    -    }
    -
    -    @Bean(name = "transactionTemplate")
    -    public TransactionTemplate transactionTemplate() {
    -        return (TransactionTemplate) BaseAppUtils.getBean("transactionTemplate");
    -    }
    -
    -    @Bean(name = "mysqlSqlFactory")
    -    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    -        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    -        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBean("dataSource");
    -        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    -        mysqlSqlFactory.setDataSource(dataSource);
    -        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    -                .getResources("classpath:mapper/*.xml"));
    -        return mysqlSqlFactory;
    -    }
    -}
    -

    SOFABoot 解法

    如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
    如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
    image.png

    
    -@Configuration
    -@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    -@EnableTransactionManagement
    -public class MybatisConfig {
    -
    -    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    -    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    -    
    -    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
    -
    -    @Bean(name = "transactionManager")
    -    public PlatformTransactionManager platformTransactionManager() {
    -        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    -    }
    -
    -    @Bean(name = "transactionTemplate")
    -    public TransactionTemplate transactionTemplate() {
    -        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    -    }
    -
    -    @Bean(name = "mysqlSqlFactory")
    -    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    -        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    -        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
    -        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    -        mysqlSqlFactory.setDataSource(dataSource);
    -        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    -                .getResources("classpath:mapper/*.xml"));
    -        return mysqlSqlFactory;
    -    }
    -}
    -


    4.8 - 静态合并部署

    介绍

    SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 + + + +

    + +
    +
    +
    +
    +
    + + + + + +
    +
    +

    +这是本节的多页打印视图。 +点击此处打印. +

    +返回本页常规视图. +

    +
    + + + +

    用户手册

    + + + + + + + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    + +

    1 - 基座接入

    + + +
    + + + + + + + + + + + + + + + + + + + +
    + +

    1.1 - SpringBoot 或 SOFABoot 升级为基座

    + +

    前提条件

    +
      +
    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. +
    3. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
    4. +
    +

    接入步骤

    +

    代码与配置修改

    +

    修改 application.properties

    +
    # 需要定义应用名
    +spring.application.name = ${替换为实际基座应用名}
    +

    修改主 pom.xml

    +
    <properties>
    +    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    +    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    +</properties>
    +
    <dependency>
    +    <groupId>com.alipay.sofa.serverless</groupId>
    +    <artifactId>sofa-serverless-base-starter</artifactId>
    +    <version>${sofa.serverless.runtime.version}</version>
    +</dependency>
    +
    +<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
    +<dependency>
    +    <groupId>com.alipay.sofa</groupId>
    +    <artifactId>web-ark-plugin</artifactId>
    +</dependency>
    +

    启动验证

    +

    基座应用能正常启动即表示验证成功!

    +
    +
    + +
    + + + + + + + + + + + + + + + +
    + +

    2 - 模块接入

    + + +
    + + + + + + + + + + + + + + + + + + + +
    + +

    2.1 - SpringBoot 或 SOFABoot 一键升级为模块

    + +

    本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

    +

    前提条件

    +
      +
    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. +
    3. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
    4. +
    +

    接入步骤

    +

    步骤 1:修改 application.properties

    +
    # 需要定义应用名
    +spring.application.name = ${替换为实际模块应用名}
    +

    步骤 2:添加模块需要的依赖和打包插件

    +

    特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

    +
    
    +<plugins>
    +    <!--这里添加ark 打包插件-->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>{sofa.ark.version}</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>${替换为模块名}</bizName>
    +            <webContextPath>${模块自定义的 web context path}</webContextPath>
    +            <declaredMode>true</declaredMode>
    +        </configuration>
    +    </plugin>
    +    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    +    <plugin>
    +        <!--原来 spring-boot 打包插件 -->
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-maven-plugin</artifactId>
    +    </plugin>
    +</plugins>
    +

    步骤 3:自动化瘦身模块

    +

    您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 +扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

    +

    步骤 4:构建成模块 jar 包

    +

    执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

    +

    小贴士模块中支持的完整中间件清单

    +

    实验:验证模块既能独立启动,也能被合并部署

    +

    增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 +当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

    +

    验证能合并部署到基座上

    +
      +
    1. 启动上一步(验证能独立启动步骤)的基座
    2. +
    3. 发起模块部署
    4. +
    +
    curl --location --request POST 'localhost:1238/installBiz' \
    +--header 'Content-Type: application/json' \
    +--data '{
    +    "bizName": "${模块名}",
    +    "bizVersion": "${模块版本}",
    +    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
    +}'
    +

    返回如下信息表示模块安装成功
    image.png

    +
      +
    1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider
    2. +
    +

    image.png

    +
      +
    1. 卸载模块
    2. +
    +
    curl --location --request POST 'localhost:1238/uninstallBiz' \
    +--header 'Content-Type: application/json' \
    +--data '{
    +    "bizName": "dynamic-provider",
    +    "bizVersion": "0.0.1-SNAPSHOT"
    +}'
    +

    返回如下,表示卸载成功

    +
    {
    +    "code": "SUCCESS",
    +    "data": {
    +        "code": "SUCCESS",
    +        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    +    }
    +}
    +

    验证能独立启动

    +

    普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
    image.png

    + +
    + + + + + + + + + + + +
    + +

    2.2 - 使用 maven archtype 脚手架自动生成

    + +

    正在更新中,预计 11 月上线。

    + +
    + + + + + + + + + + + + + + + +
    + +

    3 - 基座与模块并行开发验证

    + +

    欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 +首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

    +
      +
    1. 合并部署
    2. +
    3. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)
    4. +
    +
    +

    本文实验工程代码在:开源仓库 samples 目录库里

    +
    +

    场景一:合并部署

    +

    先介绍第一个场景多应用合并部署,整体流程如下: +image.png

    +

    可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

    +

    1. 基座接入改造

    +
      +
    1. 为 **application.properties **增加应用名(如果没有的话):
    2. +
    +

    spring.application.name=${基座应用名}

    +
      +
    1. 在 **pom.xml **里增加必要的依赖
    2. +
    +
    <properties>
    +    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    +</properties>
    +<dependencies>
    +    <dependency>
    +        <groupId>com.alipay.sofa.serverless</groupId>
    +        <artifactId>sofa-serverless-base-starter</artifactId>
    +        <version>${sofa.serverless.runtime.version}</version>
    +    </dependency>
    +</dependencies>
    +

    +

    理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

    +
        <dependency>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>web-ark-plugin</artifactId>
    +    </dependency>
    +
      +
    1. 点击编译器启动基座。
    2. +
    +

    2. 模块 1 接入改造

    +
      +
    1. 添加模块需要的依赖和打包插件
    2. +
    +
    <plugins>
    +    <!--这里添加ark 打包插件-->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>${替换为模块名}</bizName>
    +            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
    +            <declaredMode>true</declaredMode>
    +            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
    +            <packExcludesConfig>rules.txt</packExcludesConfig>
    +        </configuration>
    +    </plugin>
    +    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    +    <plugin>
    +        <!--原来 spring-boot 打包插件 -->
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-maven-plugin</artifactId>
    +    </plugin>
    +</plugins>
    +
      +
    1. +

      参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

      +
    2. +
    3. +

      开发模块,例如增加 Rest Controller,提供 Rest 接口

      +
    4. +
    +
    @RestController
    +public class SampleController {
    +    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    +
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +        String appName = applicationContext.getApplicationName();
    +        LOGGER.info("{} web test: into sample controller", appName);
    +        return String.format("hello to %s deploy", appName);
    +    }
    +}
    +
      +
    1. +

      点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

      +
    2. +
    3. +

      执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

      +
    4. +
    +
    hello to ${模块1名} deploy
    +

    3. 模块 1 开发与验证

    +

    开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

    +
      +
    1. 修改 Rest 代码
    2. +
    +
    @RestController
    +public class SampleController {
    +    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    +
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +        String appName = applicationContext.getApplicationName();
    +        LOGGER.info("{} web test v2: into sample controller", appName);
    +        return String.format("hello to %s deploy v2", appName);
    +    }
    +}
    +
      +
    1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
    2. +
    +
    hello to ${模块1名} deploy v2
    +

    4. 模块 2 接入改造、开发与验证

    +

    模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

    +

    场景二:中台应用

    +

    中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: +image.png

    +

    可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI。 +接下来我们将介绍与合并部署不同的(即新增的)操作细节。

    +

    1. 基座完成通信类和 SPI 的定义

    +

    在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 +通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

    +
      +
    1. 新建 bundle,定义接口类
    2. +
    +
    public class ProductInfo {
    +    private String  name;
    +    private String  author;
    +    private String  src;
    +    private Integer orderCount;
    +}
    +
      +
    1. 定义 SPI
    2. +
    +
    public interface StrategyService {
    +    List<ProductInfo> strategy(List<ProductInfo> products);
    +    String getAppName();
    +}
    +

    2. 模块 1 引入通信类基座并实现基座 SPI

    +

    在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

    +
      +
    1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
    2. +
    3. 定义 SPI 实现
    4. +
    +
    @Service
    +public class StrategyServiceImpl implements StrategyService {
    +
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @Override
    +    public List<ProductInfo> strategy(List<ProductInfo> products) {
    +        return products;
    +    }
    +
    +    @Override
    +    public String getAppName() {
    +        return applicationContext.getApplicationName();
    +    }
    +}
    +
      +
    1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回
    2. +
    +

    biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

    +
    默认的 products 列表
    +

    3. 模块 2 引入通信类基座并实现基座 SPI

    +

    与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

    +
    @Service
    +public class StrategyServiceImpl implements StrategyService {
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @Override
    +    public List<ProductInfo> strategy(List<ProductInfo> products) {
    +        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
    +        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
    +        return products;
    +    }
    +
    +    @Override
    +    public String getAppName() {
    +        return applicationContext.getApplicationName();
    +    }
    +}
    +
    更改排序后的 products 列表
    +

    基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

    +

    文档中的链接地址

    +
      +
    1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
    2. +
    3. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
    4. +
    5. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
    6. +
    7. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
    8. +
    9. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/
    10. +
    + +
    + + + + + + + + + + + +
    + +

    4 - 模块研发

    + + +
    + + + + + + + + + + + + + + + + + + + +
    + +

    4.1 - 编码规范

    + +

    基础规范

    +
      +
    1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
    2. +
    3. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
    4. +
    5. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布
    6. +
    +

    知识点

    +

    模块瘦身 (重要)
    +模块与模块、模块与基座通信 (重要)
    +模块测试 (重要)
    +模块复用基座拦截器
    +模块复用基座数据源
    +基座与模块间类委托加载原理介绍

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4.2 - 模块瘦身

    + +

    为什么要瘦身

    +

    为了让模块安装更快、内存消耗更小:

    +
      +
    • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
    • +
    • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。
    • +
    +

    一键自动瘦身

    +

    瘦身原则

    +

    构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

    +

    步骤一

    +

    在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

    +
    excludeGroupIds=org.apache*
    +excludeArtifactIds=commons-lang
    +

    步骤二

    +

    在模块打包插件中,引入上述配置文件:

    +
        <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>2.2.5</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>biz1</bizName>
    +            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
    +            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
    +            <packExcludesConfig>rules.txt</packExcludesConfig>
    +            <webContextPath>biz1</webContextPath>
    +            <declaredMode>true</declaredMode>
    +            <!--					打包、安装和发布 ark biz-->
    +            <!--					静态合并部署需要配置-->
    +            <!--					<attach>true</attach>-->
    +        </configuration>
    +    </plugin>
    +

    步骤三

    +

    打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

    +

    您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

    +

    基本原理

    +

    SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

    +
      +
    1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
    2. +
    3. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。
    4. +
    +

    +

    基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
    所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

    +

    手动排包瘦身

    +

    模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
    所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
    image.png

    +

    如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
    image.png
    确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
    image.png

    +

    在 pom 中统一排包(更彻底)

    +

    有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

    +
    <dependency>
    +    <groupId>com.serverless.mymodule</groupId>
    +    <artifactId>mymodule-core</artifactId>
    +    <exclusions>
    +          <exclusion>
    +              <groupId>org.springframework</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +          <exclusion>
    +              <groupId>org.apache.commons</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +          <exclusion>
    +              <groupId>......</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +    </exclusions>
    +</dependency>
    +

    在 sofa-ark-maven-plugin 中指定排包

    +

    通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

    +
     <plugin>
    +      <groupId>com.alipay.sofa</groupId>
    +      <artifactId>sofa-ark-maven-plugin</artifactId>
    +      <executions>
    +          <execution>
    +              <id>default-cli</id>
    +              <goals>
    +                  <goal>repackage</goal>
    +              </goals>
    +          </execution>
    +      </executions>
    +      <configuration>
    +          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
    +          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
    +          <outputDirectory>../../target</outputDirectory>
    +          <bizName>mymodule</bizName>
    +          <finalName>mymodule-${project.version}-${timestamp}</finalName>
    +          <bizVersion>${project.version}-${timestamp}</bizVersion>
    +          <webContextPath>/mymodule</webContextPath>
    +      </configuration>
    +  </plugin>
    +

    +
    + +
    + + + + + + + + + + + +
    + +

    4.3 - 模块与模块、模块与基座通信

    + +

    基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 +当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

    +

    Spring 环境

    +

    基座调用模块

    +

    只能使用 SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    +                "studentProvider", Provider.class);
    +        Result result = studentProvider.provide(new Param());
    +
    +        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    +                "teacherProvider", Provider.class);
    +        Result result1 = teacherProvider.provide(new Param());
    +        
    +        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
    +                Provider.class);
    +        for (String beanName : providerMap.keySet()) {
    +            Result result2 = providerMap.get(beanName).provide(new Param());
    +        }
    +
    +        return "hello to ark master biz";
    +    }
    +}
    +

    模块调用基座

    +

    方式一:注解 @AutowiredFromBase

    +
    @RestController
    +public class SampleController {
    +
    +    @AutowiredFromBase(name = "sampleServiceImplNew")
    +    private SampleService sampleServiceImplNew;
    +
    +    @AutowiredFromBase(name = "sampleServiceImpl")
    +    private SampleService sampleServiceImpl;
    +
    +    @AutowiredFromBase
    +    private List<SampleService> sampleServiceList;
    +
    +    @AutowiredFromBase
    +    private Map<String, SampleService> sampleServiceMap;
    +
    +    @AutowiredFromBase
    +    private AppService appService;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        sampleServiceImplNew.service();
    +
    +        sampleServiceImpl.service();
    +
    +        for (SampleService sampleService : sampleServiceList) {
    +            sampleService.service();
    +        }
    +
    +        for (String beanName : sampleServiceMap.keySet()) {
    +            sampleServiceMap.get(beanName).service();
    +        }
    +
    +        appService.getAppName();
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    方式二:编程API SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
    +        String result = sampleServiceImplFromFinder.service();
    +        System.out.println(result);
    +
    +        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
    +        for (String beanName : sampleServiceMapFromFinder.keySet()) {
    +            String result1 = sampleServiceMapFromFinder.get(beanName).service();
    +            System.out.println(result1);
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    模块调用模块

    +

    参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

    +

    方式一:注解 @AutowiredFromBiz

    +
    @RestController
    +public class SampleController {
    +
    +    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    +    private Provider studentProvider;
    +
    +    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    +    private Provider teacherProvider;
    +
    +    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    +    private List<Provider> providers;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Result provide = studentProvider.provide(new Param());
    +
    +        Result provide1 = teacherProvider.provide(new Param());
    +
    +        for (Provider provider : providers) {
    +            Result provide2 = provider.provide(new Param());
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    方式二:编程API SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
    +        Result result1 = teacherProvider1.provide(new Param());
    +
    +        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
    +        for (String beanName : providerMap.keySet()) {
    +            Result result2 = providerMap.get(beanName).provide(new Param());
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    完整样例

    +

    SOFABoot 环境

    +

    请参考该文档

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4.4 - 模块本地开发

    + +

    Arkctl 工具安装

    +

    方法一:

    +
      +
    1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
    2. +
    3. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。
    4. +
    +

    方法二:

    +
      +
    1. 二进制列表 中下载对应的二进制并加入到本地 +path 中。
    2. +
    +

    本地快速部署

    +

    你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

    +

    场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 打开一个模块项目仓库。
    4. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 准备一个构建好的 jar 包。
    4. +
    +

    执行命令:

    +
    arkctl deploy /path/to/your/pre/built/bundle-biz.jar
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

    +

    准备:

    +
      +
    1. 在远程已经运行起来的基座 pod。
    2. +
    3. 打开一个模块项目仓库。
    4. +
    5. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。
    6. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy --pod {namespace}/{podName}
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 打开一个多模块 Maven 项目仓库。
    4. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy --sub ./path/to/your/sub/module
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 5: 查询当前基座中已经部署的模块。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    +

    执行命令:

    +
    arkctl status
    +

    场景 6: 查询远程 k8s 环境基座中已经部署的模块。

    +

    准备:

    +
      +
    1. 在远程 k8s 环境启动一个基座。
    2. +
    3. 确保本地有 kube 证书以及有关权限。
    4. +
    +

    执行命令:

    +
    arkctl status --pod {namespace}/{name}
    +
    +
    + + + + + + + + + + + +
    + +

    4.5 - 模块测试

    + +

    本地调试

    +

    您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
    Arklet HTTP 接口主要提供了以下能力:

    +
      +
    1. 部署和卸载模块。
    2. +
    3. 查询所有已部署的模块信息。
    4. +
    5. 查询各项系统和业务指标。
    6. +
    +

    部署模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
    +

    请求体样例:

    +
    {
    +    "bizName": "test",
    +    "bizVersion": "1.0.0",
    +    // local path should start with file://, alse support remote url which can be downloaded
    +    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
    +}
    +

    部署成功返回结果样例:

    +
    {
    +  "code":"SUCCESS",
    +  "data":{
    +    "bizInfos":[
    +      {
    +        "bizName":"dynamic-provider",
    +        "bizState":"ACTIVATED",
    +        "bizVersion":"1.0.0",
    +        "declaredMode":true,
    +        "identity":"dynamic-provider:1.0.0",
    +        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    +        "priority":100,
    +        "webContextPath":"provider"
    +      }
    +    ],
    +    "code":"SUCCESS",
    +    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
    +  }
    +}
    +

    部署失败返回结果样例:

    +
    {
    +  "code":"FAILED",
    +  "data":{
    +    "code":"REPEAT_BIZ",
    +    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
    +  }
    +}
    +

    卸载模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
    +

    请求体样例:

    +
    {
    +    "bizName":"dynamic-provider",
    +    "bizVersion":"1.0.0"
    +}
    +

    卸载成功返回结果样例:

    +
    {
    +  "code":"SUCCESS"
    +}
    +

    卸载失败返回结果样例:

    +
    {
    +  "code":"FAILED",
    +  "data":{
    +    "code":"NOT_FOUND_BIZ",
    +    "message":"Uninstall biz: test:1.0.0 not found."
    +  }
    +}
    +

    查询模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
    +

    请求体样例:

    +
    {}
    +

    返回结果样例:

    +
    {
    +  "code":"SUCCESS",
    +  "data":[
    +    {
    +      "bizName":"dynamic-provider",
    +      "bizState":"ACTIVATED",
    +      "bizVersion":"1.0.0",
    +      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    +      "webContextPath":"provider"
    +    },
    +    {
    +      "bizName":"stock-mng",
    +      "bizState":"ACTIVATED",
    +      "bizVersion":"1.0.0",
    +      "mainClass":"embed main",
    +      "webContextPath":"/"
    +    }
    +  ]
    +}
    +

    获取帮助

    +

    Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
    +

    请求体样例:

    +
    {}
    +

    返回结果样例:

    +
    {
    +    "code":"SUCCESS",
    +    "data":[
    +        {
    +            "desc":"query all ark biz(including master biz)",
    +            "id":"queryAllBiz"
    +        },
    +        {
    +            "desc":"list all supported commands",
    +            "id":"help"
    +        },
    +        {
    +            "desc":"uninstall one ark biz",
    +            "id":"uninstallBiz"
    +        },
    +        {
    +            "desc":"switch one ark biz",
    +            "id":"switchBiz"
    +        },
    +        {
    +            "desc":"install one ark biz",
    +            "id":"installBiz"
    +        }
    +    ]
    +}
    +

    本地构建如何不改变模块版本号

    +

    添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

    +
    <profile>
    +    <id>local</id>
    +    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>com.alipay.sofa</groupId>
    +                <artifactId>sofa-ark-maven-plugin</artifactId>
    +                <configuration>
    +                    <finalName>${project.artifactId}-${project.version}</finalName>
    +                    <bizVersion>${project.version}</bizVersion>
    +                </configuration>
    +            </plugin>
    +        </plugins>
    +    </build>
    +</profile>
    +

    单元测试

    +

    模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4.6 - 复用基座拦截器

    + +

    诉求

    +

    基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

    +

    解法

    +

    为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

    +

    步骤 1:

    +

    基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

    +
    public interface AnnotionService {
    +    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
    +}
    +

    步骤 2:

    +

    在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

    +
    @Service
    +@SofaService(uniqueId = "facadeAroundHandler")
    +public class FacadeAroundHandler implements AnnotionService {
    +
    +    private final static Logger LOG = LoggerConst.MY_LOGGER;
    +
    +    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    +        log.info("开始执行")
    +        joinPoint.proceed();
    +        log.info("执行完成")
    +    }
    +}
    +

    步骤 3:

    +

    在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
    注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

    +
    //注意,这里不必申明成一个bean,不要加@Component或者@Service
    +@Aspect
    +public class FacadeAroundAspect {
    +
    +    @SofaReference(uniqueId = "facadeAroundHandler")
    +    private AnnotionService facadeAroundHandler;
    +
    +    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    +    public void facadeAroundPointcut() {
    +    }
    +
    +    @Around("facadeAroundPointcut()")
    +    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    +        return facadeAroundHandler.doAround(joinPoint);
    +    }
    +}
    +

    步骤 4:

    +

    使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
    注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

    +
    @Configuration
    +public class MngAspectConfiguration {
    +    @Bean
    +    public FacadeAroundAspect facadeAroundAspect() {
    +        return new FacadeAroundAspect();
    +    }
    +    @Bean
    +    public EnvRouteAspect envRouteAspect() {
    +        return new EnvRouteAspect();
    +    }
    +    @Bean
    +    public FacadeAroundAspect facadeAroundAspect() {
    +        return new FacadeAroundAspect();
    +    }
    +    
    +}
    +

    步骤 5:

    +

    模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

    +
    @SpringBootApplication
    +@ImportResource("classpath*:META-INF/spring/*.xml")
    +@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
    +public class ModuleBootstrapApplication {
    +    public static void main(String[] args) {
    +        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
    +        	.web(WebApplicationType.NONE);
    +        builder.build().run(args);
    +    }
    +}
    +

    +
    + +
    + + + + + + + + + + + +
    + +

    4.7 - 复用基座数据源

    + +

    建议

    +

    强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

    +

    SpringBoot 解法

    +

    在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
    参考 demo:/sofa-serverless/samples/springboot-samples/db/mybatis/biz1

    +

    通过SpringBeanFinder.getBaseBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

    +
    
    +@Configuration
    +@MapperScan(basePackages = "com.alipay.sofa.biz1.mapper", sqlSessionFactoryRef = "mysqlSqlFactory")
    +@EnableTransactionManagement
    +public class MybatisConfig {
    +
    +    //tips:不要初始化一个基座的DataSource,当模块被卸载的是,基座数据源会被销毁,transactionManager,transactionTemplate,mysqlSqlFactory被销毁没有问题
    +
    +    @Bean(name = "transactionManager")
    +    public PlatformTransactionManager platformTransactionManager() {
    +        return (PlatformTransactionManager) getBaseBean("transactionManager");
    +    }
    +
    +    @Bean(name = "transactionTemplate")
    +    public TransactionTemplate transactionTemplate() {
    +        return (TransactionTemplate) getBaseBean("transactionTemplate");
    +    }
    +
    +    @Bean(name = "mysqlSqlFactory")
    +    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    +        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    +
    +        DataSource dataSource = (DataSource) getBaseBean("dataSource");
    +        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    +        mysqlSqlFactory.setDataSource(dataSource);
    +        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    +                .getResources("classpath:mappers/*.xml"));
    +        return mysqlSqlFactory;
    +    }
    +}
    +

    SOFABoot 解法

    +

    如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
    如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
    image.png

    +
    
    +@Configuration
    +@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    +@EnableTransactionManagement
    +public class MybatisConfig {
    +
    +    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    +    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    +    
    +    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
    +
    +    @Bean(name = "transactionManager")
    +    public PlatformTransactionManager platformTransactionManager() {
    +        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    +    }
    +
    +    @Bean(name = "transactionTemplate")
    +    public TransactionTemplate transactionTemplate() {
    +        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    +    }
    +
    +    @Bean(name = "mysqlSqlFactory")
    +    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    +        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    +        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
    +        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    +        mysqlSqlFactory.setDataSource(dataSource);
    +        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    +                .getResources("classpath:mapper/*.xml"));
    +        return mysqlSqlFactory;
    +    }
    +}
    +

    +
    + +
    + + + + + + + + + + + +
    + +

    4.8 - 静态合并部署

    + +

    介绍

    +

    SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 用户可以把 Biz 包统一放置在某个目录中,然后通过启动参数告知基座扫描这个目录,以此完成静态合并部署(详情见下描述)。如此,开发不需要考虑相互之间依赖冲突问题,Biz 之间则通过 @SofaService 和 @SofaReference 发布/引用 JVM 服务(SOFABoot,SpringBoot 还在建设中 -)进行交互。

    步骤 1:模块应用打包成 Ark Biz

    如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz -包,详见 Ark Biz 介绍。 Ark Biz 包使用 -Maven 插件 sofa-ark-maven-plugin 打包生成。

    
    -<build>
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>${sofa.ark.version}</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -    </plugin>
    -</build>
    -

    步骤 2:将上述 jar 包移动到指定目录。

    把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

    mv /path/to/your/biz/jar /home/sofa-ark/biz/
    -

    步骤 3:启动基座,并通过 -D 参指定 biz 目录

    java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
    -

    步骤 4:验证 Ark Biz(模块)启动

    在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

    telnet localhost 1234
    -

    然后执行如下命令查看模块列表:

    biz -a
    -

    此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
    上述操作可以通过 SOFAArk 静态合并部署样例 -体验。



    4.9 - 模块中官方支持的中间件客户端

    在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
    注意,这里 “已经支持” 需要在基座 POM +)进行交互。

    +

    步骤 1:模块应用打包成 Ark Biz

    +

    如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz +包,详见 Ark Biz 介绍。 Ark Biz 包使用 +Maven 插件 sofa-ark-maven-plugin 打包生成。

    +
    
    +<build>
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>${sofa.ark.version}</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +    </plugin>
    +</build>
    +

    步骤 2:将上述 jar 包移动到指定目录。

    +

    把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

    +
    mv /path/to/your/biz/jar /home/sofa-ark/biz/
    +

    步骤 3:启动基座,并通过 -D 参指定 biz 目录

    +
    java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
    +

    步骤 4:验证 Ark Biz(模块)启动

    +

    在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

    +
    telnet localhost 1234
    +

    然后执行如下命令查看模块列表:

    +
    biz -a
    +

    此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
    +上述操作可以通过 SOFAArk 静态合并部署样例 +体验。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4.9 - 模块中官方支持的中间件客户端

    + +

    在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
    注意,这里 “已经支持” 需要在基座 POM 中引入相关客户端依赖(强烈建议使用 SpringBoot Starter 方式引入相关依赖),同时在模块 POM 中也引入相关依赖并设置 * -provided* 将依赖委托给基座加载。


    中间件客户端版本号备注
    JDK8.x
    17.x
    已经支持
    SpringBoot>= 2.3.0 或 3.x已经支持
    JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
    SOFABoot>= 3.9.0 或 4.x已经支持
    JMXN/A已经支持
    需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
    log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
    <dependency>
      <groupId>com.alipay.sofa.serverless</groupId>
      <artifactId>sofa-serverless-adapter-log4j2</artifactId>
      <version>${最新版 SOFAServerless 版本}</version>
      <scope>provided</scope> <!– 模块需要 provided –>
      </dependency>
    基座和模块完整使用样例参见此处
    slf4j-api1.x 且 >= 1.7已经支持
    tomcat7.x、8.x、9.x
    及以上均可; 10.x 暂不支持
    已经支持
    netty4.x已经支持
    sofarpc>= 5.8.6已经支持
    dubbo3.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    grpc1.x 且 >= 1.42已经支持
    基座和模块完整使用样例及注意事项可参见此处
    protobuf-java3.x 且 >= 3.17已经支持
    基座和模块完整使用样例及注意事项可参见此处
    apollo1.x 且 >= 1.6.0已经支持
    基座和模块完整使用样例及注意事项可参见此处
    kafka-client>= 2.8.0 或
    >= 3.4.0
    已经支持
    基座和模块完整使用样例可参见此处
    rocketmq4.x 且 >= 4.3.0已经支持
    基座和模块完整使用样例可参见此处
    jedis3.x已经支持
    基座和模块完整使用样例可参见此处
    xxl-job2.x 且 >= 2.1.0已经支持
    需要在模块里声明为 compile 依赖独立使用
    mybatis>= 2.2.2 或
    >= 3.5.12
    已经支持
    基座和模块完整使用样例可参见此处
    druid1.x已经支持
    基座和模块完整使用样例可参见此处
    mysql-connector-java8.x已经支持
    基座和模块完整使用样例可参见此处
    postgresql42.x 且 >= 42.3.8已经支持
    mongodb4.6.1已经支持
    基座和模块完整使用样例可参见此处
    hibernate5.x 且 >= 5.6.15已经支持
    j2cache任意已经支持
    需要在模块里声明为 compile 依赖独立使用
    opentracing0.x 且 >= 0.32.0已经支持
    elasticsearch7.x 且 >= 7.6.2已经支持
    jaspyt1.x 且 >= 1.9.3治理进行中
    OKHttp-已经支持
    需要放在基座里,请使用模块自动瘦身能力
    io.kubernetes:client10.x 且 >= 10.0.0已经支持
    net.java.dev.jna5.x 且 >= 5.12.1已经支持

    5 - 模块运维

    5.1 - 模块上线与下线

    模块上线

    在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2
    -  operationStrategy:  # 此处可自定义发布运维策略
    -    upgradePolicy: installThenUninstall
    -    needConfirm: true
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy: # 此处可自定义调度策略
    -    schedulingPolicy: Scatter
    -

    ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

    模块下线

    在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

    kubectl delete yourmoduledeployment --namespace yournamespace
    -

    其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。



    5.2 - 模块发布

    模块发布

    修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
    -        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2
    -  operationStrategy:
    -    upgradePolicy: install_then_uninstall
    -    needConfirm: true
    -    grayTimeBetweenBatchSeconds: 0
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy:
    -    schedulingPolicy: scatter
    -

    如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

    模块回滚

    重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。



    5.3 - 基座和模块不兼容发布

    步骤 1

    修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

    步骤 2

    修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

    步骤 3

    使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。



    5.4 - 模块扩缩容与替换

    模块扩缩容

    修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
    -  operationStrategy:
    -    upgradePolicy: installThenUninstall
    -    needConfirm: true
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy: # 此处可自定义调度策略
    -    schedulingPolicy: Scatter  
    -

    如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

    模块替换

    在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

    kubectl delete yourmodule --namespace yournamespace
    -

    其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。



    5.5 - 模块发布运维策略

    运维策略

    为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

    字段名字段解释取值范围取值解释
    batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
    useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
    false 表示不启用 beta 分组
    needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
    false 表示不启用分组确认
    grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次

    调度策略

    可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
    打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
    堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

    保护机制

    (正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

    对等和非对等

    您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
    非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
    对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。



    5.6 - 独立使用 Arklet

    Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处



    5.7 - 模块信息查看

    查看某个基座上所有安装的模块名称和状态

    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    -

    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    -

    查看某个基座上所有安装的模块详细信息

    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
    -

    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
    -

    替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

    5.8 - 模块Service

    ModuleService 简介

    K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 +provided* 将依赖委托给基座加载。

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    中间件客户端版本号备注
    JDK8.x
    17.x
    已经支持
    SpringBoot>= 2.3.0 或 3.x已经支持
    JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
    SpringBoot Cloud>= 2.7.x已经支持
    基座和模块完整使用样例可参见此处
    SOFABoot>= 3.9.0 或 4.x已经支持
    JMXN/A已经支持
    需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
    log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
    <dependency>
      <groupId>com.alipay.sofa.serverless</groupId>
      <artifactId>sofa-serverless-adapter-log4j2</artifactId>
      <version>${最新版 SOFAServerless 版本}</version>
      <scope>provided</scope> <!– 模块需要 provided –>
      </dependency>
    基座和模块完整使用样例参见此处
    slf4j-api1.x 且 >= 1.7已经支持
    tomcat7.x、8.x、9.x、10.x
    及以上均可
    已经支持
    基座和模块完整使用样例可参见此处
    netty4.x已经支持
    基座和模块完整使用样例可参见此处
    sofarpc>= 5.8.6已经支持
    dubbo3.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    grpc1.x 且 >= 1.42已经支持
    基座和模块完整使用样例及注意事项可参见此处
    protobuf-java3.x 且 >= 3.17已经支持
    基座和模块完整使用样例及注意事项可参见此处
    apollo1.x 且 >= 1.6.0已经支持
    基座和模块完整使用样例及注意事项可参见此处
    nacos2.1.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    kafka-client>= 2.8.0 或
    >= 3.4.0
    已经支持
    基座和模块完整使用样例可参见此处
    rocketmq4.x 且 >= 4.3.0已经支持
    基座和模块完整使用样例可参见此处
    jedis3.x已经支持
    基座和模块完整使用样例可参见此处
    xxl-job2.x 且 >= 2.1.0已经支持
    需要在模块里声明为 compile 依赖独立使用
    mybatis>= 2.2.2 或
    >= 3.5.12
    已经支持
    基座和模块完整使用样例可参见此处
    druid1.x已经支持
    基座和模块完整使用样例可参见此处
    mysql-connector-java8.x已经支持
    基座和模块完整使用样例可参见此处
    postgresql42.x 且 >= 42.3.8已经支持
    mongodb4.6.1已经支持
    基座和模块完整使用样例可参见此处
    hibernate5.x 且 >= 5.6.15已经支持
    j2cache任意已经支持
    需要在模块里声明为 compile 依赖独立使用
    opentracing0.x 且 >= 0.32.0已经支持
    elasticsearch7.x 且 >= 7.6.2已经支持
    jaspyt1.x 且 >= 1.9.3已经支持
    OKHttp-已经支持
    需要放在基座里,请使用模块自动瘦身能力
    io.kubernetes:client10.x 且 >= 10.0.0已经支持
    net.java.dev.jna5.x 且 >= 5.12.1已经支持
    prometheus-待验证支持
    + +
    + + + + + + + + + + + +
    + +

    4.10 - SOFAArk 关键用户文档

    + +

    模块生命周期

    +Ark 事件机制

    +Ark 自身日志

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4.11 -

    + + +
    + + + + + + + + + + + + + + + +
    + +

    5 - 模块运维

    + + +
    + + + + + + + + + + + + + + + + + + + +
    + +

    5.1 - 模块上线与下线

    + +

    模块上线

    +

    在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2
    +  operationStrategy:  # 此处可自定义发布运维策略
    +    upgradePolicy: installThenUninstall
    +    needConfirm: true
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy: # 此处可自定义调度策略
    +    schedulingPolicy: Scatter
    +

    ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

    +

    模块下线

    +

    在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

    +
    kubectl delete yourmoduledeployment --namespace yournamespace
    +

    其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5.2 - 模块发布

    + +

    模块发布

    +

    修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
    +        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2
    +  operationStrategy:
    +    upgradePolicy: install_then_uninstall
    +    needConfirm: true
    +    grayTimeBetweenBatchSeconds: 0
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy:
    +    schedulingPolicy: scatter
    +

    如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

    +

    模块回滚

    +

    重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5.3 - 基座和模块不兼容发布

    + +

    步骤 1

    +

    修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

    +

    步骤 2

    +

    修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

    +

    步骤 3

    +

    使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5.4 - 模块扩缩容与替换

    + +

    模块扩缩容

    +

    修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
    +  operationStrategy:
    +    upgradePolicy: installThenUninstall
    +    needConfirm: true
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy: # 此处可自定义调度策略
    +    schedulingPolicy: Scatter  
    +

    如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

    +

    模块替换

    +

    在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

    +
    kubectl delete yourmodule --namespace yournamespace
    +

    其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5.5 - 模块发布运维策略

    + +

    运维策略

    +

    为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    字段名字段解释取值范围取值解释
    batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
    useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
    false 表示不启用 beta 分组
    needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
    false 表示不启用分组确认
    grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次
    +

    调度策略

    +

    可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
    +打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
    +堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

    +

    保护机制

    +

    (正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

    +

    对等和非对等

    +

    您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
    +非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
    +对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5.6 - 独立使用 Arklet

    + +

    Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5.7 - 模块信息查看

    + +

    查看某个基座上所有安装的模块名称和状态

    +
    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    +

    +
    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    +

    查看某个基座上所有安装的模块详细信息

    +
    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
    +

    +
    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
    +

    替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

    + +
    + + + + + + + + + + + +
    + +

    5.8 - 模块Service

    + +

    ModuleService 简介

    +

    K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 模块也支持 Module 相关的 Service ,在模块发布时自动创建一个 service 来服务模块,将安装在一个或一组 Pod 的模块公开为网络服务。 -具体见:OperationStrategy.ServiceStrategy

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample-provider
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 1
    -  operationStrategy:
    -    needConfirm: false
    -    grayTimeBetweenBatchSeconds: 120
    -    useBeta: false
    -    batchCount: 1
    -    upgradePolicy: install_then_uninstall
    -    serviceStrategy:
    -      enableModuleService: true
    -      port: 8080
    -      targetPort: 8080
    -  schedulingStrategy:
    -    schedulingPolicy: scatter
    -

    字段解释

    OperationStrategy.ServiceStrategy 字段解释如下:

    字段解释取值范围
    EnableModuleService开启模块servicetrue or false
    Port公开的端口1 到 65535
    TargetPortpod上要访问的端口1 到 65535

    示例

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
    -

    自动创建的模块的 service

    apiVersion: v1
    -kind: Service
    -metadata:
    -  creationTimestamp: "2023-11-03T09:52:22Z"
    -  name: dynamic-provider-service
    -  namespace: default
    -  resourceVersion: "28170024"
    -  uid: 1f85e468-65e3-4181-b40e-48959a069df5
    -spec:
    -  clusterIP: 10.0.147.22
    -  clusterIPs:
    -  - 10.0.147.22
    -  externalTrafficPolicy: Cluster
    -  internalTrafficPolicy: Cluster
    -  ipFamilies:
    -  - IPv4
    -  ipFamilyPolicy: SingleStack
    -  ports:
    -  - name: http
    -    nodePort: 32232
    -    port: 8080
    -    protocol: TCP
    -    targetPort: 8080
    -  selector:
    -    module.serverless.alipay.com/dynamic-provider: "true"
    -  sessionAffinity: None
    -  type: NodePort
    -status:
    -  loadBalancer: {}
    -

    5.9 - 所有 K8S 资源定义及部署方式

    资源文件位置

    1. ModuleDeployment CRD 定义
    2. ModuleReplicaset CRD 定义
    3. ModuleTemplate CRD 定义
    4. Module CRD 定义
    5. Role 定义
    6. RBAC 定义
    7. ServiceAccount 定义
    8. ModuleController 部署定义

    部署方式

    使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。



    - - \ No newline at end of file +具体见:OperationStrategy.ServiceStrategy

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample-provider
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 1
    +  operationStrategy:
    +    needConfirm: false
    +    grayTimeBetweenBatchSeconds: 120
    +    useBeta: false
    +    batchCount: 1
    +    upgradePolicy: install_then_uninstall
    +    serviceStrategy:
    +      enableModuleService: true
    +      port: 8080
    +      targetPort: 8080
    +  schedulingStrategy:
    +    schedulingPolicy: scatter
    +

    字段解释

    +

    OperationStrategy.ServiceStrategy 字段解释如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    字段解释取值范围
    EnableModuleService开启模块servicetrue or false
    Port公开的端口1 到 65535
    TargetPortpod上要访问的端口1 到 65535
    +

    示例

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
    +

    自动创建的模块的 service

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  creationTimestamp: "2023-11-03T09:52:22Z"
    +  name: dynamic-provider-service
    +  namespace: default
    +  resourceVersion: "28170024"
    +  uid: 1f85e468-65e3-4181-b40e-48959a069df5
    +spec:
    +  clusterIP: 10.0.147.22
    +  clusterIPs:
    +  - 10.0.147.22
    +  externalTrafficPolicy: Cluster
    +  internalTrafficPolicy: Cluster
    +  ipFamilies:
    +  - IPv4
    +  ipFamilyPolicy: SingleStack
    +  ports:
    +  - name: http
    +    nodePort: 32232
    +    port: 8080
    +    protocol: TCP
    +    targetPort: 8080
    +  selector:
    +    module.serverless.alipay.com/dynamic-provider: "true"
    +  sessionAffinity: None
    +  type: NodePort
    +status:
    +  loadBalancer: {}
    +
    +
    + + + + + + + + + + + +
    + +

    5.9 - 所有 K8S 资源定义及部署方式

    + +

    资源文件位置

    +
      +
    1. ModuleDeployment CRD 定义
    2. +
    3. ModuleReplicaset CRD 定义
    4. +
    5. ModuleTemplate CRD 定义
    6. +
    7. Module CRD 定义
    8. +
    9. Role 定义
    10. +
    11. RBAC 定义
    12. +
    13. ServiceAccount 定义
    14. +
    15. ModuleController 部署定义
    16. +
    +

    部署方式

    +

    使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。

    +
    +
    +
    + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + diff --git a/docs/public/docs/tutorials/base-create/_print/index.html b/docs/public/docs/tutorials/base-create/_print/index.html index db9302085..325af07c7 100644 --- a/docs/public/docs/tutorials/base-create/_print/index.html +++ b/docs/public/docs/tutorials/base-create/_print/index.html @@ -1,25 +1,285 @@ -基座接入 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +基座接入 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    这是本节的多页打印视图。 -点击此处打印.

    返回本页常规视图.

    基座接入

    1 - SpringBoot 或 SOFABoot 升级为基座

    前提条件

    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

    接入步骤

    代码与配置修改

    修改 application.properties

    # 需要定义应用名
    -spring.application.name = ${替换为实际基座应用名}
    -

    修改主 pom.xml

    <properties>
    -    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    -    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    -</properties>
    -
    <dependency>
    -    <groupId>com.alipay.sofa.serverless</groupId>
    -    <artifactId>sofa-serverless-base-starter</artifactId>
    -    <version>${sofa.serverless.runtime.version}</version>
    -</dependency>
    -
    -<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
    -<dependency>
    -    <groupId>com.alipay.sofa</groupId>
    -    <artifactId>web-ark-plugin</artifactId>
    -</dependency>
    -

    启动验证

    基座应用能正常启动即表示验证成功!



    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    +
    + + + + + +
    +
    +

    +这是本节的多页打印视图。 +点击此处打印. +

    +返回本页常规视图. +

    +
    + + + +

    基座接入

    + + + + + + + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    + +

    1 - SpringBoot 或 SOFABoot 升级为基座

    + +

    前提条件

    +
      +
    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. +
    3. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
    4. +
    +

    接入步骤

    +

    代码与配置修改

    +

    修改 application.properties

    +
    # 需要定义应用名
    +spring.application.name = ${替换为实际基座应用名}
    +

    修改主 pom.xml

    +
    <properties>
    +    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    +    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    +</properties>
    +
    <dependency>
    +    <groupId>com.alipay.sofa.serverless</groupId>
    +    <artifactId>sofa-serverless-base-starter</artifactId>
    +    <version>${sofa.serverless.runtime.version}</version>
    +</dependency>
    +
    +<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
    +<dependency>
    +    <groupId>com.alipay.sofa</groupId>
    +    <artifactId>web-ark-plugin</artifactId>
    +</dependency>
    +

    启动验证

    +

    基座应用能正常启动即表示验证成功!

    +
    +
    + +
    + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + diff --git a/docs/public/docs/tutorials/base-create/index.html b/docs/public/docs/tutorials/base-create/index.html index 73fab9b96..44a50797b 100644 --- a/docs/public/docs/tutorials/base-create/index.html +++ b/docs/public/docs/tutorials/base-create/index.html @@ -1,12 +1,494 @@ -基座接入 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +基座接入 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    基座接入

    -

    最后修改 November 14, 2023: add build and deploy (1950dff0)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    基座接入

    + + + +
    + + + + + + + + +
    + + +
    +
    + SpringBoot 或 SOFABoot 升级为基座 +
    +

    +
    + + +
    + +
    + + + + + + +
    + +
    +
    + 最后修改 November 14, 2023: add build and deploy (1950dff0) +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html b/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html index 5463cd54d..90bcb17be 100644 --- a/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html +++ b/docs/public/docs/tutorials/base-create/springboot-and-sofaboot/index.html @@ -1,29 +1,523 @@ -SpringBoot 或 SOFABoot 升级为基座 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +SpringBoot 或 SOFABoot 升级为基座 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    SpringBoot 或 SOFABoot 升级为基座

    前提条件

    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

    接入步骤

    代码与配置修改

    修改 application.properties

    # 需要定义应用名
    -spring.application.name = ${替换为实际基座应用名}
    -

    修改主 pom.xml

    <properties>
    -    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    -    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    -</properties>
    -
    <dependency>
    -    <groupId>com.alipay.sofa.serverless</groupId>
    -    <artifactId>sofa-serverless-base-starter</artifactId>
    -    <version>${sofa.serverless.runtime.version}</version>
    -</dependency>
    -
    -<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
    -<dependency>
    -    <groupId>com.alipay.sofa</groupId>
    -    <artifactId>web-ark-plugin</artifactId>
    -</dependency>
    -

    启动验证

    基座应用能正常启动即表示验证成功!



    -

    最后修改 November 19, 2023: update version to 0.5.3 (3b998aac)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    SpringBoot 或 SOFABoot 升级为基座

    + + +

    前提条件

    +
      +
    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. +
    3. SOFABoot 版本 >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
    4. +
    +

    接入步骤

    +

    代码与配置修改

    +

    修改 application.properties

    +
    # 需要定义应用名
    +spring.application.name = ${替换为实际基座应用名}
    +

    修改主 pom.xml

    +
    <properties>
    +    <sofa.ark.verion>2.2.5</sofa.ark.verion>
    +    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    +</properties>
    +
    <dependency>
    +    <groupId>com.alipay.sofa.serverless</groupId>
    +    <artifactId>sofa-serverless-base-starter</artifactId>
    +    <version>${sofa.serverless.runtime.version}</version>
    +</dependency>
    +
    +<!-- 如果使用了 springboot web,则加上这个依赖,详细查看https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/ -->
    +<dependency>
    +    <groupId>com.alipay.sofa</groupId>
    +    <artifactId>web-ark-plugin</artifactId>
    +</dependency>
    +

    启动验证

    +

    基座应用能正常启动即表示验证成功!

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 December 12, 2023: update ark to 2.2.5 (155856d0) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/index.html b/docs/public/docs/tutorials/index.html index 19350b81e..9798cc4ca 100644 --- a/docs/public/docs/tutorials/index.html +++ b/docs/public/docs/tutorials/index.html @@ -1,12 +1,526 @@ -用户手册 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +用户手册 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    用户手册

    -

    最后修改 September 22, 2023: add docs (efcf6b56)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    用户手册

    + + + +
    + + + + + + + + +
    + + +
    +
    + 基座接入 +
    +

    +
    + + +
    +
    + 模块接入 +
    +

    +
    + + +
    +
    + 基座与模块并行开发验证 +
    +

    +
    + + +
    +
    + 模块研发 +
    +

    +
    + + +
    +
    + 模块运维 +
    +

    +
    + + +
    + +
    + + + + + + +
    + +
    +
    + 最后修改 September 22, 2023: add docs (efcf6b56) +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-create/_print/index.html b/docs/public/docs/tutorials/module-create/_print/index.html index e5c1a1a90..56cc21cdc 100644 --- a/docs/public/docs/tutorials/module-create/_print/index.html +++ b/docs/public/docs/tutorials/module-create/_print/index.html @@ -1,62 +1,368 @@ -模块接入 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +模块接入 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    1 - SpringBoot 或 SOFABoot 一键升级为模块

    本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

    前提条件

    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

    接入步骤

    步骤 1:修改 application.properties

    # 需要定义应用名
    -spring.application.name = ${替换为实际模块应用名}
    -

    步骤 2:添加模块需要的依赖和打包插件

    特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

    
    -<plugins>
    -    <!--这里添加ark 打包插件-->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>{sofa.ark.version}</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>${替换为模块名}</bizName>
    -            <webContextPath>${模块自定义的 web context path}</webContextPath>
    -            <declaredMode>true</declaredMode>
    -        </configuration>
    -    </plugin>
    -    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    -    <plugin>
    -        <!--原来 spring-boot 打包插件 -->
    -        <groupId>org.springframework.boot</groupId>
    -        <artifactId>spring-boot-maven-plugin</artifactId>
    -    </plugin>
    -</plugins>
    -

    步骤 3:自动化瘦身模块

    您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 -扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

    步骤 4:构建成模块 jar 包

    执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

    小贴士模块中支持的完整中间件清单

    实验:验证模块既能独立启动,也能被合并部署

    增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 -当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

    验证能合并部署到基座上

    1. 启动上一步(验证能独立启动步骤)的基座
    2. 发起模块部署
    curl --location --request POST 'localhost:1238/installBiz' \
    ---header 'Content-Type: application/json' \
    ---data '{
    -    "bizName": "${模块名}",
    -    "bizVersion": "${模块版本}",
    -    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
    -}'
    -

    返回如下信息表示模块安装成功
    image.png

    1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider

    image.png

    1. 卸载模块
    curl --location --request POST 'localhost:1238/uninstallBiz' \
    ---header 'Content-Type: application/json' \
    ---data '{
    -    "bizName": "dynamic-provider",
    -    "bizVersion": "0.0.1-SNAPSHOT"
    -}'
    -

    返回如下,表示卸载成功

    {
    -    "code": "SUCCESS",
    -    "data": {
    -        "code": "SUCCESS",
    -        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    -    }
    -}
    -

    验证能独立启动

    普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
    image.png

    2 - 使用 maven archtype 脚手架自动生成

    正在更新中,预计 11 月上线。

    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    +
    + + + + + +
    +
    +

    +这是本节的多页打印视图。 +点击此处打印. +

    +返回本页常规视图. +

    +
    + + + +

    模块接入

    + + + + + + + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    + +

    1 - SpringBoot 或 SOFABoot 一键升级为模块

    + +

    本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

    +

    前提条件

    +
      +
    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. +
    3. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
    4. +
    +

    接入步骤

    +

    步骤 1:修改 application.properties

    +
    # 需要定义应用名
    +spring.application.name = ${替换为实际模块应用名}
    +

    步骤 2:添加模块需要的依赖和打包插件

    +

    特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

    +
    
    +<plugins>
    +    <!--这里添加ark 打包插件-->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>{sofa.ark.version}</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>${替换为模块名}</bizName>
    +            <webContextPath>${模块自定义的 web context path}</webContextPath>
    +            <declaredMode>true</declaredMode>
    +        </configuration>
    +    </plugin>
    +    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    +    <plugin>
    +        <!--原来 spring-boot 打包插件 -->
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-maven-plugin</artifactId>
    +    </plugin>
    +</plugins>
    +

    步骤 3:自动化瘦身模块

    +

    您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 +扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

    +

    步骤 4:构建成模块 jar 包

    +

    执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

    +

    小贴士模块中支持的完整中间件清单

    +

    实验:验证模块既能独立启动,也能被合并部署

    +

    增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 +当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

    +

    验证能合并部署到基座上

    +
      +
    1. 启动上一步(验证能独立启动步骤)的基座
    2. +
    3. 发起模块部署
    4. +
    +
    curl --location --request POST 'localhost:1238/installBiz' \
    +--header 'Content-Type: application/json' \
    +--data '{
    +    "bizName": "${模块名}",
    +    "bizVersion": "${模块版本}",
    +    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
    +}'
    +

    返回如下信息表示模块安装成功
    image.png

    +
      +
    1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider
    2. +
    +

    image.png

    +
      +
    1. 卸载模块
    2. +
    +
    curl --location --request POST 'localhost:1238/uninstallBiz' \
    +--header 'Content-Type: application/json' \
    +--data '{
    +    "bizName": "dynamic-provider",
    +    "bizVersion": "0.0.1-SNAPSHOT"
    +}'
    +

    返回如下,表示卸载成功

    +
    {
    +    "code": "SUCCESS",
    +    "data": {
    +        "code": "SUCCESS",
    +        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    +    }
    +}
    +

    验证能独立启动

    +

    普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
    image.png

    + +
    + + + + + + + + + + + +
    + +

    2 - 使用 maven archtype 脚手架自动生成

    + +

    正在更新中,预计 11 月上线。

    + +
    + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + diff --git a/docs/public/docs/tutorials/module-create/index.html b/docs/public/docs/tutorials/module-create/index.html index 33d8272da..26a5e315d 100644 --- a/docs/public/docs/tutorials/module-create/index.html +++ b/docs/public/docs/tutorials/module-create/index.html @@ -1,12 +1,502 @@ -模块接入 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +模块接入 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    模块接入

    -

    最后修改 November 14, 2023: add build and deploy (1950dff0)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块接入

    + + + + + +
    + + + + + + +
    + +
    +
    + 最后修改 November 14, 2023: add build and deploy (1950dff0) +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-create/init_by_archtype/index.html b/docs/public/docs/tutorials/module-create/init_by_archtype/index.html index 5b251a56e..cc21b427a 100644 --- a/docs/public/docs/tutorials/module-create/init_by_archtype/index.html +++ b/docs/public/docs/tutorials/module-create/init_by_archtype/index.html @@ -1,12 +1,481 @@ -使用 maven archtype 脚手架自动生成 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +使用 maven archtype 脚手架自动生成 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    使用 maven archtype 脚手架自动生成

    正在更新中,预计 11 月上线。

    -

    最后修改 November 14, 2023: update docs (8bb39d7f)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    使用 maven archtype 脚手架自动生成

    + + +

    正在更新中,预计 11 月上线。

    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 14, 2023: update docs (8bb39d7f) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-create/springboot-and-sofaboot/index.html b/docs/public/docs/tutorials/module-create/springboot-and-sofaboot/index.html index 4e2bc9b81..9c98ce411 100644 --- a/docs/public/docs/tutorials/module-create/springboot-and-sofaboot/index.html +++ b/docs/public/docs/tutorials/module-create/springboot-and-sofaboot/index.html @@ -1,74 +1,596 @@ -SpringBoot 或 SOFABoot 一键升级为模块 | SOFAServerless + + + + + + + + + + + + + + + + + + +SpringBoot 或 SOFABoot 一键升级为模块 | SOFAServerless + - - +&lt;plugins&gt; &lt;!--这里添加ark 打包插件--&gt; &lt;plugin&gt; &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt; &lt;artifactId&gt;sofa-ark-maven-plugin&lt;/artifactId&gt; &lt;version&gt;{sofa.ark.version}&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;id&gt;default-cli&lt;/id&gt; &lt;goals&gt; &lt;goal&gt;repackage&lt;/goal&gt; &lt;/goals&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;configuration&gt; &lt;skipArkExecutable&gt;true&lt;/skipArkExecutable&gt; &lt;outputDirectory&gt;./target&lt;/outputDirectory&gt; &lt;bizName&gt;${替换为模块名}&lt;/bizName&gt; &lt;webContextPath&gt;${模块自定义的 web context path}&lt;/webContextPath&gt; &lt;declaredMode&gt;true&lt;/declaredMode&gt; &lt;/configuration&gt; &lt;/plugin&gt; &lt;!-- 构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除 --&gt; &lt;plugin&gt; &lt;!"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    SpringBoot 或 SOFABoot 一键升级为模块

    本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

    前提条件

    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)

    接入步骤

    步骤 1:修改 application.properties

    # 需要定义应用名
    -spring.application.name = ${替换为实际模块应用名}
    -

    步骤 2:添加模块需要的依赖和打包插件

    特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

    
    -<plugins>
    -    <!--这里添加ark 打包插件-->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>{sofa.ark.version}</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>${替换为模块名}</bizName>
    -            <webContextPath>${模块自定义的 web context path}</webContextPath>
    -            <declaredMode>true</declaredMode>
    -        </configuration>
    -    </plugin>
    -    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    -    <plugin>
    -        <!--原来 spring-boot 打包插件 -->
    -        <groupId>org.springframework.boot</groupId>
    -        <artifactId>spring-boot-maven-plugin</artifactId>
    -    </plugin>
    -</plugins>
    -

    步骤 3:自动化瘦身模块

    您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 -扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

    步骤 4:构建成模块 jar 包

    执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

    小贴士模块中支持的完整中间件清单

    实验:验证模块既能独立启动,也能被合并部署

    增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 -当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

    验证能合并部署到基座上

    1. 启动上一步(验证能独立启动步骤)的基座
    2. 发起模块部署
    curl --location --request POST 'localhost:1238/installBiz' \
    ---header 'Content-Type: application/json' \
    ---data '{
    -    "bizName": "${模块名}",
    -    "bizVersion": "${模块版本}",
    -    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
    -}'
    -

    返回如下信息表示模块安装成功
    image.png

    1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider

    image.png

    1. 卸载模块
    curl --location --request POST 'localhost:1238/uninstallBiz' \
    ---header 'Content-Type: application/json' \
    ---data '{
    -    "bizName": "dynamic-provider",
    -    "bizVersion": "0.0.1-SNAPSHOT"
    -}'
    -

    返回如下,表示卸载成功

    {
    -    "code": "SUCCESS",
    -    "data": {
    -        "code": "SUCCESS",
    -        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    -    }
    -}
    -

    验证能独立启动

    普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
    image.png

    -

    最后修改 November 27, 2023: add build plugin comment (4468d6d4)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    SpringBoot 或 SOFABoot 一键升级为模块

    + + +

    本文讲解了 SpringBoot 或 SOFABoot 一键升级为模块的操作和验证步骤,仅需加一个 ark 打包插件即可实现普通应用一键升级为模块应用,并且能做到同一套代码分支,既能像原来 SpringBoot 一样独立启动,也能作为模块与其它应用合并部署在一起启动。

    +

    前提条件

    +
      +
    1. SpringBoot 版本 >= 2.3.0(针对 SpringBoot 用户)
    2. +
    3. SOFABoot >= 3.9.0 或 SOFABoot >= 4.0.0(针对 SOFABoot 用户)
    4. +
    +

    接入步骤

    +

    步骤 1:修改 application.properties

    +
    # 需要定义应用名
    +spring.application.name = ${替换为实际模块应用名}
    +

    步骤 2:添加模块需要的依赖和打包插件

    +

    特别注意: sofa ark 插件定义顺序必须在 springboot 打包插件前;

    +
    
    +<plugins>
    +    <!--这里添加ark 打包插件-->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>{sofa.ark.version}</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>${替换为模块名}</bizName>
    +            <webContextPath>${模块自定义的 web context path}</webContextPath>
    +            <declaredMode>true</declaredMode>
    +        </configuration>
    +    </plugin>
    +    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    +    <plugin>
    +        <!--原来 spring-boot 打包插件 -->
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-maven-plugin</artifactId>
    +    </plugin>
    +</plugins>
    +

    步骤 3:自动化瘦身模块

    +

    您可以使用 ark 打包插件的自动化瘦身能力,自动化瘦身模块应用里的 maven 依赖。这一步是必选的,否则构建出的模块 jar 包会非常大,而且启动会报错。 +扩展阅读:如果模块不做依赖瘦身独立引入 SpringBoot 框架会怎样?

    +

    步骤 4:构建成模块 jar 包

    +

    执行 mvn clean package -DskipTest, 可以在 target 目录下找到打包生成的 ark biz jar 包,也可以在 target/boot 目录下找到打包生成的普通的 springboot jar 包。

    +

    小贴士模块中支持的完整中间件清单

    +

    实验:验证模块既能独立启动,也能被合并部署

    +

    增加模块打包插件(sofa-ark-maven-plugin)进行打包后,只会新增 ark-biz.jar 构建产物,与原生 spring-boot-maven-plugin 打包的可执行Jar 互相不冲突、不影响。 +当服务器部署时,期望独立启动,就使用原生 spring-boot-maven-plugin 构建出的可执行 Jar 作为构建产物;期望作为 ark 模块部署到基座中时,就使用 sofa-ark-maven-plugin 构建出的 xxx-ark-biz.jar 作为构建产物

    +

    验证能合并部署到基座上

    +
      +
    1. 启动上一步(验证能独立启动步骤)的基座
    2. +
    3. 发起模块部署
    4. +
    +
    curl --location --request POST 'localhost:1238/installBiz' \
    +--header 'Content-Type: application/json' \
    +--data '{
    +    "bizName": "${模块名}",
    +    "bizVersion": "${模块版本}",
    +    "bizUrl": "file:///path/to/ark/biz/jar/target/xx-xxxx-ark-biz.jar"
    +}'
    +

    返回如下信息表示模块安装成功
    image.png

    +
      +
    1. 查看当前模块信息,除了基座 base 以外,还存在一个模块 dynamic-provider
    2. +
    +

    image.png

    +
      +
    1. 卸载模块
    2. +
    +
    curl --location --request POST 'localhost:1238/uninstallBiz' \
    +--header 'Content-Type: application/json' \
    +--data '{
    +    "bizName": "dynamic-provider",
    +    "bizVersion": "0.0.1-SNAPSHOT"
    +}'
    +

    返回如下,表示卸载成功

    +
    {
    +    "code": "SUCCESS",
    +    "data": {
    +        "code": "SUCCESS",
    +        "message": "Uninstall biz: dynamic-provider:0.0.1-SNAPSHOT success."
    +    }
    +}
    +

    验证能独立启动

    +

    普通应用改造成模块之后,还是可以独立启动,可以验证一些基本的启动逻辑,只需要在启动配置里勾选自动添加 providedscope 到 classPath 即可,后启动方式与普通应用方式一致。通过自动瘦身改造的模块,也可以在 target/boot 目录下直接通过 springboot jar 包启动,点击此处查看详情。
    image.png

    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 27, 2023: add build plugin comment (4468d6d4) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/_print/index.html b/docs/public/docs/tutorials/module-development/_print/index.html index 284b7934e..f98de16bf 100644 --- a/docs/public/docs/tutorials/module-development/_print/index.html +++ b/docs/public/docs/tutorials/module-development/_print/index.html @@ -1,470 +1,1339 @@ -模块研发 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +模块研发 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    1 - 编码规范

    基础规范

    1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
    2. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
    3. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布

    知识点

    模块瘦身 (重要)
    模块与模块、模块与基座通信 (重要)
    模块测试 (重要)
    模块复用基座拦截器
    模块复用基座数据源
    基座与模块间类委托加载原理介绍



    2 - 模块瘦身

    为什么要瘦身

    为了让模块安装更快、内存消耗更小:

    • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
    • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。

    一键自动瘦身

    瘦身原则

    构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

    步骤一

    在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

    excludeGroupIds=org.apache*
    -excludeArtifactIds=commons-lang
    -

    步骤二

    在模块打包插件中,引入上述配置文件:

        <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>2.2.5</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>biz1</bizName>
    -            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
    -            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
    -            <packExcludesConfig>rules.txt</packExcludesConfig>
    -            <webContextPath>biz1</webContextPath>
    -            <declaredMode>true</declaredMode>
    -            <!--					打包、安装和发布 ark biz-->
    -            <!--					静态合并部署需要配置-->
    -            <!--					<attach>true</attach>-->
    -        </configuration>
    -    </plugin>
    -

    步骤三

    打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

    您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

    基本原理

    SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

    1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
    2. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。

    基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
    所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

    手动排包瘦身

    模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
    所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
    image.png

    如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
    image.png
    确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
    image.png

    在 pom 中统一排包(更彻底)

    有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

    <dependency>
    -    <groupId>com.serverless.mymodule</groupId>
    -    <artifactId>mymodule-core</artifactId>
    -    <exclusions>
    -          <exclusion>
    -              <groupId>org.springframework</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -          <exclusion>
    -              <groupId>org.apache.commons</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -          <exclusion>
    -              <groupId>......</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -    </exclusions>
    -</dependency>
    -

    在 sofa-ark-maven-plugin 中指定排包

    通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

     <plugin>
    -      <groupId>com.alipay.sofa</groupId>
    -      <artifactId>sofa-ark-maven-plugin</artifactId>
    -      <executions>
    -          <execution>
    -              <id>default-cli</id>
    -              <goals>
    -                  <goal>repackage</goal>
    -              </goals>
    -          </execution>
    -      </executions>
    -      <configuration>
    -          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
    -          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
    -          <outputDirectory>../../target</outputDirectory>
    -          <bizName>mymodule</bizName>
    -          <finalName>mymodule-${project.version}-${timestamp}</finalName>
    -          <bizVersion>${project.version}-${timestamp}</bizVersion>
    -          <webContextPath>/mymodule</webContextPath>
    -      </configuration>
    -  </plugin>
    -


    3 - 模块与模块、模块与基座通信

    基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 -当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

    Spring 环境

    基座调用模块

    只能使用 SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    -                "studentProvider", Provider.class);
    -        Result result = studentProvider.provide(new Param());
    -
    -        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    -                "teacherProvider", Provider.class);
    -        Result result1 = teacherProvider.provide(new Param());
    -        
    -        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
    -                Provider.class);
    -        for (String beanName : providerMap.keySet()) {
    -            Result result2 = providerMap.get(beanName).provide(new Param());
    -        }
    -
    -        return "hello to ark master biz";
    -    }
    -}
    -

    模块调用基座

    方式一:注解 @AutowiredFromBase

    @RestController
    -public class SampleController {
    -
    -    @AutowiredFromBase(name = "sampleServiceImplNew")
    -    private SampleService sampleServiceImplNew;
    -
    -    @AutowiredFromBase(name = "sampleServiceImpl")
    -    private SampleService sampleServiceImpl;
    -
    -    @AutowiredFromBase
    -    private List<SampleService> sampleServiceList;
    -
    -    @AutowiredFromBase
    -    private Map<String, SampleService> sampleServiceMap;
    -
    -    @AutowiredFromBase
    -    private AppService appService;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        sampleServiceImplNew.service();
    -
    -        sampleServiceImpl.service();
    -
    -        for (SampleService sampleService : sampleServiceList) {
    -            sampleService.service();
    -        }
    -
    -        for (String beanName : sampleServiceMap.keySet()) {
    -            sampleServiceMap.get(beanName).service();
    -        }
    -
    -        appService.getAppName();
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    方式二:编程API SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
    -        String result = sampleServiceImplFromFinder.service();
    -        System.out.println(result);
    -
    -        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
    -        for (String beanName : sampleServiceMapFromFinder.keySet()) {
    -            String result1 = sampleServiceMapFromFinder.get(beanName).service();
    -            System.out.println(result1);
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    模块调用模块

    参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

    方式一:注解 @AutowiredFromBiz

    @RestController
    -public class SampleController {
    -
    -    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    -    private Provider studentProvider;
    -
    -    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    -    private Provider teacherProvider;
    -
    -    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    -    private List<Provider> providers;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Result provide = studentProvider.provide(new Param());
    -
    -        Result provide1 = teacherProvider.provide(new Param());
    -
    -        for (Provider provider : providers) {
    -            Result provide2 = provider.provide(new Param());
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    方式二:编程API SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
    -        Result result1 = teacherProvider1.provide(new Param());
    -
    -        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
    -        for (String beanName : providerMap.keySet()) {
    -            Result result2 = providerMap.get(beanName).provide(new Param());
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    完整样例

    SOFABoot 环境

    请参考该文档



    4 - 模式本地开发

    Arkctl 工具安装

    方法一:

    1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
    2. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。

    方法二:

    1. 二进制列表 中下载对应的二进制并加入到本地 -path 中。

    本地快速部署

    你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

    场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

    准备:

    1. 在本地启动一个基座。
    2. 打开一个模块项目仓库。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

    准备:

    1. 在本地启动一个基座。
    2. 准备一个构建好的 jar 包。

    执行命令:

    arkctl deploy /path/to/your/pre/built/bundle-biz.jar
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

    准备:

    1. 在远程已经运行起来的基座 pod。
    2. 打开一个模块项目仓库。
    3. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy --pod {namespace}/{podName}
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

    准备:

    1. 在本地启动一个基座。
    2. 打开一个多模块 Maven 项目仓库。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy --sub ./path/to/your/sub/module
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 5: 查询当前基座中已经部署的模块。

    准备:

    1. 在本地启动一个基座。

    执行命令:

    arkctl status
    -

    场景 6: 查询远程 k8s 环境基座中已经部署的模块。

    准备:

    1. 在远程 k8s 环境启动一个基座。
    2. 确保本地有 kube 证书以及有关权限。

    执行命令:

    arkctl status --pod {namespace}/{name}
    -

    5 - 模式测试

    本地调试

    您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
    Arklet HTTP 接口主要提供了以下能力:

    1. 部署和卸载模块。
    2. 查询所有已部署的模块信息。
    3. 查询各项系统和业务指标。

    部署模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
    -

    请求体样例:

    {
    -    "bizName": "test",
    -    "bizVersion": "1.0.0",
    -    // local path should start with file://, alse support remote url which can be downloaded
    -    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
    -}
    -

    部署成功返回结果样例:

    {
    -  "code":"SUCCESS",
    -  "data":{
    -    "bizInfos":[
    -      {
    -        "bizName":"dynamic-provider",
    -        "bizState":"ACTIVATED",
    -        "bizVersion":"1.0.0",
    -        "declaredMode":true,
    -        "identity":"dynamic-provider:1.0.0",
    -        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    -        "priority":100,
    -        "webContextPath":"provider"
    -      }
    -    ],
    -    "code":"SUCCESS",
    -    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
    -  }
    -}
    -

    部署失败返回结果样例:

    {
    -  "code":"FAILED",
    -  "data":{
    -    "code":"REPEAT_BIZ",
    -    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
    -  }
    -}
    -

    卸载模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
    -

    请求体样例:

    {
    -    "bizName":"dynamic-provider",
    -    "bizVersion":"1.0.0"
    -}
    -

    卸载成功返回结果样例:

    {
    -  "code":"SUCCESS"
    -}
    -

    卸载失败返回结果样例:

    {
    -  "code":"FAILED",
    -  "data":{
    -    "code":"NOT_FOUND_BIZ",
    -    "message":"Uninstall biz: test:1.0.0 not found."
    -  }
    -}
    -

    查询模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
    -

    请求体样例:

    {}
    -

    返回结果样例:

    {
    -  "code":"SUCCESS",
    -  "data":[
    -    {
    -      "bizName":"dynamic-provider",
    -      "bizState":"ACTIVATED",
    -      "bizVersion":"1.0.0",
    -      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    -      "webContextPath":"provider"
    -    },
    -    {
    -      "bizName":"stock-mng",
    -      "bizState":"ACTIVATED",
    -      "bizVersion":"1.0.0",
    -      "mainClass":"embed main",
    -      "webContextPath":"/"
    -    }
    -  ]
    -}
    -

    获取帮助

    Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
    -

    请求体样例:

    {}
    -

    返回结果样例:

    {
    -    "code":"SUCCESS",
    -    "data":[
    -        {
    -            "desc":"query all ark biz(including master biz)",
    -            "id":"queryAllBiz"
    -        },
    -        {
    -            "desc":"list all supported commands",
    -            "id":"help"
    -        },
    -        {
    -            "desc":"uninstall one ark biz",
    -            "id":"uninstallBiz"
    -        },
    -        {
    -            "desc":"switch one ark biz",
    -            "id":"switchBiz"
    -        },
    -        {
    -            "desc":"install one ark biz",
    -            "id":"installBiz"
    -        }
    -    ]
    -}
    -

    本地构建如何不改变模块版本号

    添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

    <profile>
    -    <id>local</id>
    -    <build>
    -        <plugins>
    -            <plugin>
    -                <groupId>com.alipay.sofa</groupId>
    -                <artifactId>sofa-ark-maven-plugin</artifactId>
    -                <configuration>
    -                    <finalName>${project.artifactId}-${project.version}</finalName>
    -                    <bizVersion>${project.version}</bizVersion>
    -                </configuration>
    -            </plugin>
    -        </plugins>
    -    </build>
    -</profile>
    -

    单元测试

    模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。



    6 - 复用基座拦截器

    诉求

    基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

    解法

    为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

    步骤 1:

    基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

    public interface AnnotionService {
    -    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
    -}
    -

    步骤 2:

    在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

    @Service
    -@SofaService(uniqueId = "facadeAroundHandler")
    -public class FacadeAroundHandler implements AnnotionService {
    -
    -    private final static Logger LOG = LoggerConst.MY_LOGGER;
    -
    -    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    -        log.info("开始执行")
    -        joinPoint.proceed();
    -        log.info("执行完成")
    -    }
    -}
    -

    步骤 3:

    在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
    注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

    //注意,这里不必申明成一个bean,不要加@Component或者@Service
    -@Aspect
    -public class FacadeAroundAspect {
    -
    -    @SofaReference(uniqueId = "facadeAroundHandler")
    -    private AnnotionService facadeAroundHandler;
    -
    -    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    -    public void facadeAroundPointcut() {
    -    }
    -
    -    @Around("facadeAroundPointcut()")
    -    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    -        return facadeAroundHandler.doAround(joinPoint);
    -    }
    -}
    -

    步骤 4:

    使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
    注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

    @Configuration
    -public class MngAspectConfiguration {
    -    @Bean
    -    public FacadeAroundAspect facadeAroundAspect() {
    -        return new FacadeAroundAspect();
    -    }
    -    @Bean
    -    public EnvRouteAspect envRouteAspect() {
    -        return new EnvRouteAspect();
    -    }
    -    @Bean
    -    public FacadeAroundAspect facadeAroundAspect() {
    -        return new FacadeAroundAspect();
    -    }
    -    
    -}
    -

    步骤 5:

    模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

    @SpringBootApplication
    -@ImportResource("classpath*:META-INF/spring/*.xml")
    -@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
    -public class ModuleBootstrapApplication {
    -    public static void main(String[] args) {
    -        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
    -        	.web(WebApplicationType.NONE);
    -        builder.build().run(args);
    -    }
    -}
    -


    7 - 复用基座数据源

    建议

    强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

    SpringBoot 解法

    在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
    通过BaseAppUtils.getBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

    
    -@Configuration
    -@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    -@EnableTransactionManagement
    -public class MybatisConfig {
    -
    -    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    -    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    -
    -    @Bean(name = "transactionManager")
    -    public PlatformTransactionManager platformTransactionManager() {
    -        return (PlatformTransactionManager) BaseAppUtils.getBean("transactionManager");
    -    }
    -
    -    @Bean(name = "transactionTemplate")
    -    public TransactionTemplate transactionTemplate() {
    -        return (TransactionTemplate) BaseAppUtils.getBean("transactionTemplate");
    -    }
    -
    -    @Bean(name = "mysqlSqlFactory")
    -    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    -        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    -        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBean("dataSource");
    -        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    -        mysqlSqlFactory.setDataSource(dataSource);
    -        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    -                .getResources("classpath:mapper/*.xml"));
    -        return mysqlSqlFactory;
    -    }
    -}
    -

    SOFABoot 解法

    如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
    如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
    image.png

    
    -@Configuration
    -@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    -@EnableTransactionManagement
    -public class MybatisConfig {
    -
    -    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    -    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    -    
    -    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
    -
    -    @Bean(name = "transactionManager")
    -    public PlatformTransactionManager platformTransactionManager() {
    -        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    -    }
    -
    -    @Bean(name = "transactionTemplate")
    -    public TransactionTemplate transactionTemplate() {
    -        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    -    }
    -
    -    @Bean(name = "mysqlSqlFactory")
    -    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    -        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    -        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
    -        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    -        mysqlSqlFactory.setDataSource(dataSource);
    -        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    -                .getResources("classpath:mapper/*.xml"));
    -        return mysqlSqlFactory;
    -    }
    -}
    -


    8 - 静态合并部署

    介绍

    SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 + + + +

    + +
    +
    +
    +
    +
    + + + + + +
    +
    +

    +这是本节的多页打印视图。 +点击此处打印. +

    +返回本页常规视图. +

    +
    + + + +

    模块研发

    + + + + + + + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    + +

    1 - 编码规范

    + +

    基础规范

    +
      +
    1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
    2. +
    3. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
    4. +
    5. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布
    6. +
    +

    知识点

    +

    模块瘦身 (重要)
    +模块与模块、模块与基座通信 (重要)
    +模块测试 (重要)
    +模块复用基座拦截器
    +模块复用基座数据源
    +基座与模块间类委托加载原理介绍

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    2 - 模块瘦身

    + +

    为什么要瘦身

    +

    为了让模块安装更快、内存消耗更小:

    +
      +
    • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
    • +
    • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。
    • +
    +

    一键自动瘦身

    +

    瘦身原则

    +

    构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

    +

    步骤一

    +

    在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

    +
    excludeGroupIds=org.apache*
    +excludeArtifactIds=commons-lang
    +

    步骤二

    +

    在模块打包插件中,引入上述配置文件:

    +
        <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>2.2.5</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>biz1</bizName>
    +            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
    +            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
    +            <packExcludesConfig>rules.txt</packExcludesConfig>
    +            <webContextPath>biz1</webContextPath>
    +            <declaredMode>true</declaredMode>
    +            <!--					打包、安装和发布 ark biz-->
    +            <!--					静态合并部署需要配置-->
    +            <!--					<attach>true</attach>-->
    +        </configuration>
    +    </plugin>
    +

    步骤三

    +

    打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

    +

    您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

    +

    基本原理

    +

    SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

    +
      +
    1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
    2. +
    3. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。
    4. +
    +

    +

    基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
    所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

    +

    手动排包瘦身

    +

    模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
    所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
    image.png

    +

    如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
    image.png
    确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
    image.png

    +

    在 pom 中统一排包(更彻底)

    +

    有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

    +
    <dependency>
    +    <groupId>com.serverless.mymodule</groupId>
    +    <artifactId>mymodule-core</artifactId>
    +    <exclusions>
    +          <exclusion>
    +              <groupId>org.springframework</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +          <exclusion>
    +              <groupId>org.apache.commons</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +          <exclusion>
    +              <groupId>......</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +    </exclusions>
    +</dependency>
    +

    在 sofa-ark-maven-plugin 中指定排包

    +

    通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

    +
     <plugin>
    +      <groupId>com.alipay.sofa</groupId>
    +      <artifactId>sofa-ark-maven-plugin</artifactId>
    +      <executions>
    +          <execution>
    +              <id>default-cli</id>
    +              <goals>
    +                  <goal>repackage</goal>
    +              </goals>
    +          </execution>
    +      </executions>
    +      <configuration>
    +          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
    +          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
    +          <outputDirectory>../../target</outputDirectory>
    +          <bizName>mymodule</bizName>
    +          <finalName>mymodule-${project.version}-${timestamp}</finalName>
    +          <bizVersion>${project.version}-${timestamp}</bizVersion>
    +          <webContextPath>/mymodule</webContextPath>
    +      </configuration>
    +  </plugin>
    +

    +
    + +
    + + + + + + + + + + + +
    + +

    3 - 模块与模块、模块与基座通信

    + +

    基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 +当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

    +

    Spring 环境

    +

    基座调用模块

    +

    只能使用 SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    +                "studentProvider", Provider.class);
    +        Result result = studentProvider.provide(new Param());
    +
    +        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    +                "teacherProvider", Provider.class);
    +        Result result1 = teacherProvider.provide(new Param());
    +        
    +        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
    +                Provider.class);
    +        for (String beanName : providerMap.keySet()) {
    +            Result result2 = providerMap.get(beanName).provide(new Param());
    +        }
    +
    +        return "hello to ark master biz";
    +    }
    +}
    +

    模块调用基座

    +

    方式一:注解 @AutowiredFromBase

    +
    @RestController
    +public class SampleController {
    +
    +    @AutowiredFromBase(name = "sampleServiceImplNew")
    +    private SampleService sampleServiceImplNew;
    +
    +    @AutowiredFromBase(name = "sampleServiceImpl")
    +    private SampleService sampleServiceImpl;
    +
    +    @AutowiredFromBase
    +    private List<SampleService> sampleServiceList;
    +
    +    @AutowiredFromBase
    +    private Map<String, SampleService> sampleServiceMap;
    +
    +    @AutowiredFromBase
    +    private AppService appService;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        sampleServiceImplNew.service();
    +
    +        sampleServiceImpl.service();
    +
    +        for (SampleService sampleService : sampleServiceList) {
    +            sampleService.service();
    +        }
    +
    +        for (String beanName : sampleServiceMap.keySet()) {
    +            sampleServiceMap.get(beanName).service();
    +        }
    +
    +        appService.getAppName();
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    方式二:编程API SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
    +        String result = sampleServiceImplFromFinder.service();
    +        System.out.println(result);
    +
    +        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
    +        for (String beanName : sampleServiceMapFromFinder.keySet()) {
    +            String result1 = sampleServiceMapFromFinder.get(beanName).service();
    +            System.out.println(result1);
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    模块调用模块

    +

    参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

    +

    方式一:注解 @AutowiredFromBiz

    +
    @RestController
    +public class SampleController {
    +
    +    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    +    private Provider studentProvider;
    +
    +    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    +    private Provider teacherProvider;
    +
    +    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    +    private List<Provider> providers;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Result provide = studentProvider.provide(new Param());
    +
    +        Result provide1 = teacherProvider.provide(new Param());
    +
    +        for (Provider provider : providers) {
    +            Result provide2 = provider.provide(new Param());
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    方式二:编程API SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
    +        Result result1 = teacherProvider1.provide(new Param());
    +
    +        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
    +        for (String beanName : providerMap.keySet()) {
    +            Result result2 = providerMap.get(beanName).provide(new Param());
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    完整样例

    +

    SOFABoot 环境

    +

    请参考该文档

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4 - 模块本地开发

    + +

    Arkctl 工具安装

    +

    方法一:

    +
      +
    1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
    2. +
    3. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。
    4. +
    +

    方法二:

    +
      +
    1. 二进制列表 中下载对应的二进制并加入到本地 +path 中。
    2. +
    +

    本地快速部署

    +

    你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

    +

    场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 打开一个模块项目仓库。
    4. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 准备一个构建好的 jar 包。
    4. +
    +

    执行命令:

    +
    arkctl deploy /path/to/your/pre/built/bundle-biz.jar
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

    +

    准备:

    +
      +
    1. 在远程已经运行起来的基座 pod。
    2. +
    3. 打开一个模块项目仓库。
    4. +
    5. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。
    6. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy --pod {namespace}/{podName}
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 打开一个多模块 Maven 项目仓库。
    4. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy --sub ./path/to/your/sub/module
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 5: 查询当前基座中已经部署的模块。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    +

    执行命令:

    +
    arkctl status
    +

    场景 6: 查询远程 k8s 环境基座中已经部署的模块。

    +

    准备:

    +
      +
    1. 在远程 k8s 环境启动一个基座。
    2. +
    3. 确保本地有 kube 证书以及有关权限。
    4. +
    +

    执行命令:

    +
    arkctl status --pod {namespace}/{name}
    +
    +
    + + + + + + + + + + + +
    + +

    5 - 模块测试

    + +

    本地调试

    +

    您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
    Arklet HTTP 接口主要提供了以下能力:

    +
      +
    1. 部署和卸载模块。
    2. +
    3. 查询所有已部署的模块信息。
    4. +
    5. 查询各项系统和业务指标。
    6. +
    +

    部署模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
    +

    请求体样例:

    +
    {
    +    "bizName": "test",
    +    "bizVersion": "1.0.0",
    +    // local path should start with file://, alse support remote url which can be downloaded
    +    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
    +}
    +

    部署成功返回结果样例:

    +
    {
    +  "code":"SUCCESS",
    +  "data":{
    +    "bizInfos":[
    +      {
    +        "bizName":"dynamic-provider",
    +        "bizState":"ACTIVATED",
    +        "bizVersion":"1.0.0",
    +        "declaredMode":true,
    +        "identity":"dynamic-provider:1.0.0",
    +        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    +        "priority":100,
    +        "webContextPath":"provider"
    +      }
    +    ],
    +    "code":"SUCCESS",
    +    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
    +  }
    +}
    +

    部署失败返回结果样例:

    +
    {
    +  "code":"FAILED",
    +  "data":{
    +    "code":"REPEAT_BIZ",
    +    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
    +  }
    +}
    +

    卸载模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
    +

    请求体样例:

    +
    {
    +    "bizName":"dynamic-provider",
    +    "bizVersion":"1.0.0"
    +}
    +

    卸载成功返回结果样例:

    +
    {
    +  "code":"SUCCESS"
    +}
    +

    卸载失败返回结果样例:

    +
    {
    +  "code":"FAILED",
    +  "data":{
    +    "code":"NOT_FOUND_BIZ",
    +    "message":"Uninstall biz: test:1.0.0 not found."
    +  }
    +}
    +

    查询模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
    +

    请求体样例:

    +
    {}
    +

    返回结果样例:

    +
    {
    +  "code":"SUCCESS",
    +  "data":[
    +    {
    +      "bizName":"dynamic-provider",
    +      "bizState":"ACTIVATED",
    +      "bizVersion":"1.0.0",
    +      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    +      "webContextPath":"provider"
    +    },
    +    {
    +      "bizName":"stock-mng",
    +      "bizState":"ACTIVATED",
    +      "bizVersion":"1.0.0",
    +      "mainClass":"embed main",
    +      "webContextPath":"/"
    +    }
    +  ]
    +}
    +

    获取帮助

    +

    Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
    +

    请求体样例:

    +
    {}
    +

    返回结果样例:

    +
    {
    +    "code":"SUCCESS",
    +    "data":[
    +        {
    +            "desc":"query all ark biz(including master biz)",
    +            "id":"queryAllBiz"
    +        },
    +        {
    +            "desc":"list all supported commands",
    +            "id":"help"
    +        },
    +        {
    +            "desc":"uninstall one ark biz",
    +            "id":"uninstallBiz"
    +        },
    +        {
    +            "desc":"switch one ark biz",
    +            "id":"switchBiz"
    +        },
    +        {
    +            "desc":"install one ark biz",
    +            "id":"installBiz"
    +        }
    +    ]
    +}
    +

    本地构建如何不改变模块版本号

    +

    添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

    +
    <profile>
    +    <id>local</id>
    +    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>com.alipay.sofa</groupId>
    +                <artifactId>sofa-ark-maven-plugin</artifactId>
    +                <configuration>
    +                    <finalName>${project.artifactId}-${project.version}</finalName>
    +                    <bizVersion>${project.version}</bizVersion>
    +                </configuration>
    +            </plugin>
    +        </plugins>
    +    </build>
    +</profile>
    +

    单元测试

    +

    模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    6 - 复用基座拦截器

    + +

    诉求

    +

    基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

    +

    解法

    +

    为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

    +

    步骤 1:

    +

    基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

    +
    public interface AnnotionService {
    +    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
    +}
    +

    步骤 2:

    +

    在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

    +
    @Service
    +@SofaService(uniqueId = "facadeAroundHandler")
    +public class FacadeAroundHandler implements AnnotionService {
    +
    +    private final static Logger LOG = LoggerConst.MY_LOGGER;
    +
    +    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    +        log.info("开始执行")
    +        joinPoint.proceed();
    +        log.info("执行完成")
    +    }
    +}
    +

    步骤 3:

    +

    在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
    注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

    +
    //注意,这里不必申明成一个bean,不要加@Component或者@Service
    +@Aspect
    +public class FacadeAroundAspect {
    +
    +    @SofaReference(uniqueId = "facadeAroundHandler")
    +    private AnnotionService facadeAroundHandler;
    +
    +    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    +    public void facadeAroundPointcut() {
    +    }
    +
    +    @Around("facadeAroundPointcut()")
    +    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    +        return facadeAroundHandler.doAround(joinPoint);
    +    }
    +}
    +

    步骤 4:

    +

    使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
    注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

    +
    @Configuration
    +public class MngAspectConfiguration {
    +    @Bean
    +    public FacadeAroundAspect facadeAroundAspect() {
    +        return new FacadeAroundAspect();
    +    }
    +    @Bean
    +    public EnvRouteAspect envRouteAspect() {
    +        return new EnvRouteAspect();
    +    }
    +    @Bean
    +    public FacadeAroundAspect facadeAroundAspect() {
    +        return new FacadeAroundAspect();
    +    }
    +    
    +}
    +

    步骤 5:

    +

    模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

    +
    @SpringBootApplication
    +@ImportResource("classpath*:META-INF/spring/*.xml")
    +@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
    +public class ModuleBootstrapApplication {
    +    public static void main(String[] args) {
    +        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
    +        	.web(WebApplicationType.NONE);
    +        builder.build().run(args);
    +    }
    +}
    +

    +
    + +
    + + + + + + + + + + + +
    + +

    7 - 复用基座数据源

    + +

    建议

    +

    强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

    +

    SpringBoot 解法

    +

    在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
    参考 demo:/sofa-serverless/samples/springboot-samples/db/mybatis/biz1

    +

    通过SpringBeanFinder.getBaseBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

    +
    
    +@Configuration
    +@MapperScan(basePackages = "com.alipay.sofa.biz1.mapper", sqlSessionFactoryRef = "mysqlSqlFactory")
    +@EnableTransactionManagement
    +public class MybatisConfig {
    +
    +    //tips:不要初始化一个基座的DataSource,当模块被卸载的是,基座数据源会被销毁,transactionManager,transactionTemplate,mysqlSqlFactory被销毁没有问题
    +
    +    @Bean(name = "transactionManager")
    +    public PlatformTransactionManager platformTransactionManager() {
    +        return (PlatformTransactionManager) getBaseBean("transactionManager");
    +    }
    +
    +    @Bean(name = "transactionTemplate")
    +    public TransactionTemplate transactionTemplate() {
    +        return (TransactionTemplate) getBaseBean("transactionTemplate");
    +    }
    +
    +    @Bean(name = "mysqlSqlFactory")
    +    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    +        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    +
    +        DataSource dataSource = (DataSource) getBaseBean("dataSource");
    +        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    +        mysqlSqlFactory.setDataSource(dataSource);
    +        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    +                .getResources("classpath:mappers/*.xml"));
    +        return mysqlSqlFactory;
    +    }
    +}
    +

    SOFABoot 解法

    +

    如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
    如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
    image.png

    +
    
    +@Configuration
    +@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    +@EnableTransactionManagement
    +public class MybatisConfig {
    +
    +    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    +    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    +    
    +    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
    +
    +    @Bean(name = "transactionManager")
    +    public PlatformTransactionManager platformTransactionManager() {
    +        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    +    }
    +
    +    @Bean(name = "transactionTemplate")
    +    public TransactionTemplate transactionTemplate() {
    +        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    +    }
    +
    +    @Bean(name = "mysqlSqlFactory")
    +    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    +        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    +        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
    +        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    +        mysqlSqlFactory.setDataSource(dataSource);
    +        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    +                .getResources("classpath:mapper/*.xml"));
    +        return mysqlSqlFactory;
    +    }
    +}
    +

    +
    + +
    + + + + + + + + + + + +
    + +

    8 - 静态合并部署

    + +

    介绍

    +

    SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 用户可以把 Biz 包统一放置在某个目录中,然后通过启动参数告知基座扫描这个目录,以此完成静态合并部署(详情见下描述)。如此,开发不需要考虑相互之间依赖冲突问题,Biz 之间则通过 @SofaService 和 @SofaReference 发布/引用 JVM 服务(SOFABoot,SpringBoot 还在建设中 -)进行交互。

    步骤 1:模块应用打包成 Ark Biz

    如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz -包,详见 Ark Biz 介绍。 Ark Biz 包使用 -Maven 插件 sofa-ark-maven-plugin 打包生成。

    
    -<build>
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>${sofa.ark.version}</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -    </plugin>
    -</build>
    -

    步骤 2:将上述 jar 包移动到指定目录。

    把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

    mv /path/to/your/biz/jar /home/sofa-ark/biz/
    -

    步骤 3:启动基座,并通过 -D 参指定 biz 目录

    java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
    -

    步骤 4:验证 Ark Biz(模块)启动

    在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

    telnet localhost 1234
    -

    然后执行如下命令查看模块列表:

    biz -a
    -

    此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
    上述操作可以通过 SOFAArk 静态合并部署样例 -体验。



    9 - 模块中官方支持的中间件客户端

    在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
    注意,这里 “已经支持” 需要在基座 POM +)进行交互。

    +

    步骤 1:模块应用打包成 Ark Biz

    +

    如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz +包,详见 Ark Biz 介绍。 Ark Biz 包使用 +Maven 插件 sofa-ark-maven-plugin 打包生成。

    +
    
    +<build>
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>${sofa.ark.version}</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +    </plugin>
    +</build>
    +

    步骤 2:将上述 jar 包移动到指定目录。

    +

    把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

    +
    mv /path/to/your/biz/jar /home/sofa-ark/biz/
    +

    步骤 3:启动基座,并通过 -D 参指定 biz 目录

    +
    java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
    +

    步骤 4:验证 Ark Biz(模块)启动

    +

    在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

    +
    telnet localhost 1234
    +

    然后执行如下命令查看模块列表:

    +
    biz -a
    +

    此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
    +上述操作可以通过 SOFAArk 静态合并部署样例 +体验。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    9 - 模块中官方支持的中间件客户端

    + +

    在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
    注意,这里 “已经支持” 需要在基座 POM 中引入相关客户端依赖(强烈建议使用 SpringBoot Starter 方式引入相关依赖),同时在模块 POM 中也引入相关依赖并设置 * -provided* 将依赖委托给基座加载。


    中间件客户端版本号备注
    JDK8.x
    17.x
    已经支持
    SpringBoot>= 2.3.0 或 3.x已经支持
    JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
    SOFABoot>= 3.9.0 或 4.x已经支持
    JMXN/A已经支持
    需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
    log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
    <dependency>
      <groupId>com.alipay.sofa.serverless</groupId>
      <artifactId>sofa-serverless-adapter-log4j2</artifactId>
      <version>${最新版 SOFAServerless 版本}</version>
      <scope>provided</scope> <!– 模块需要 provided –>
      </dependency>
    基座和模块完整使用样例参见此处
    slf4j-api1.x 且 >= 1.7已经支持
    tomcat7.x、8.x、9.x
    及以上均可; 10.x 暂不支持
    已经支持
    netty4.x已经支持
    sofarpc>= 5.8.6已经支持
    dubbo3.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    grpc1.x 且 >= 1.42已经支持
    基座和模块完整使用样例及注意事项可参见此处
    protobuf-java3.x 且 >= 3.17已经支持
    基座和模块完整使用样例及注意事项可参见此处
    apollo1.x 且 >= 1.6.0已经支持
    基座和模块完整使用样例及注意事项可参见此处
    kafka-client>= 2.8.0 或
    >= 3.4.0
    已经支持
    基座和模块完整使用样例可参见此处
    rocketmq4.x 且 >= 4.3.0已经支持
    基座和模块完整使用样例可参见此处
    jedis3.x已经支持
    基座和模块完整使用样例可参见此处
    xxl-job2.x 且 >= 2.1.0已经支持
    需要在模块里声明为 compile 依赖独立使用
    mybatis>= 2.2.2 或
    >= 3.5.12
    已经支持
    基座和模块完整使用样例可参见此处
    druid1.x已经支持
    基座和模块完整使用样例可参见此处
    mysql-connector-java8.x已经支持
    基座和模块完整使用样例可参见此处
    postgresql42.x 且 >= 42.3.8已经支持
    mongodb4.6.1已经支持
    基座和模块完整使用样例可参见此处
    hibernate5.x 且 >= 5.6.15已经支持
    j2cache任意已经支持
    需要在模块里声明为 compile 依赖独立使用
    opentracing0.x 且 >= 0.32.0已经支持
    elasticsearch7.x 且 >= 7.6.2已经支持
    jaspyt1.x 且 >= 1.9.3治理进行中
    OKHttp-已经支持
    需要放在基座里,请使用模块自动瘦身能力
    io.kubernetes:client10.x 且 >= 10.0.0已经支持
    net.java.dev.jna5.x 且 >= 5.12.1已经支持
    - - \ No newline at end of file +provided* 将依赖委托给基座加载。

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    中间件客户端版本号备注
    JDK8.x
    17.x
    已经支持
    SpringBoot>= 2.3.0 或 3.x已经支持
    JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
    SpringBoot Cloud>= 2.7.x已经支持
    基座和模块完整使用样例可参见此处
    SOFABoot>= 3.9.0 或 4.x已经支持
    JMXN/A已经支持
    需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
    log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
    <dependency>
      <groupId>com.alipay.sofa.serverless</groupId>
      <artifactId>sofa-serverless-adapter-log4j2</artifactId>
      <version>${最新版 SOFAServerless 版本}</version>
      <scope>provided</scope> <!– 模块需要 provided –>
      </dependency>
    基座和模块完整使用样例参见此处
    slf4j-api1.x 且 >= 1.7已经支持
    tomcat7.x、8.x、9.x、10.x
    及以上均可
    已经支持
    基座和模块完整使用样例可参见此处
    netty4.x已经支持
    基座和模块完整使用样例可参见此处
    sofarpc>= 5.8.6已经支持
    dubbo3.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    grpc1.x 且 >= 1.42已经支持
    基座和模块完整使用样例及注意事项可参见此处
    protobuf-java3.x 且 >= 3.17已经支持
    基座和模块完整使用样例及注意事项可参见此处
    apollo1.x 且 >= 1.6.0已经支持
    基座和模块完整使用样例及注意事项可参见此处
    nacos2.1.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    kafka-client>= 2.8.0 或
    >= 3.4.0
    已经支持
    基座和模块完整使用样例可参见此处
    rocketmq4.x 且 >= 4.3.0已经支持
    基座和模块完整使用样例可参见此处
    jedis3.x已经支持
    基座和模块完整使用样例可参见此处
    xxl-job2.x 且 >= 2.1.0已经支持
    需要在模块里声明为 compile 依赖独立使用
    mybatis>= 2.2.2 或
    >= 3.5.12
    已经支持
    基座和模块完整使用样例可参见此处
    druid1.x已经支持
    基座和模块完整使用样例可参见此处
    mysql-connector-java8.x已经支持
    基座和模块完整使用样例可参见此处
    postgresql42.x 且 >= 42.3.8已经支持
    mongodb4.6.1已经支持
    基座和模块完整使用样例可参见此处
    hibernate5.x 且 >= 5.6.15已经支持
    j2cache任意已经支持
    需要在模块里声明为 compile 依赖独立使用
    opentracing0.x 且 >= 0.32.0已经支持
    elasticsearch7.x 且 >= 7.6.2已经支持
    jaspyt1.x 且 >= 1.9.3已经支持
    OKHttp-已经支持
    需要放在基座里,请使用模块自动瘦身能力
    io.kubernetes:client10.x 且 >= 10.0.0已经支持
    net.java.dev.jna5.x 且 >= 5.12.1已经支持
    prometheus-待验证支持
    + +
    + + + + + + + + + + + +
    + +

    10 - SOFAArk 关键用户文档

    + +

    模块生命周期

    +Ark 事件机制

    +Ark 自身日志

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    11 -

    + + +
    + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + diff --git a/docs/public/docs/tutorials/module-development/coding-specification/index.html b/docs/public/docs/tutorials/module-development/coding-specification/index.html index d72b7ca9e..a7a91f27f 100644 --- a/docs/public/docs/tutorials/module-development/coding-specification/index.html +++ b/docs/public/docs/tutorials/module-development/coding-specification/index.html @@ -1,32 +1,523 @@ -编码规范 | SOFAServerless + + + + + + + + + + + + + + + + + + +编码规范 | SOFAServerless + + + + + + + + + + + + + + - - +基座与模块间类委托加载原理介绍"/> + + + + + + + + + + + + + + + + + + + -

    编码规范

    基础规范

    1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
    2. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
    3. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布

    知识点

    模块瘦身 (重要)
    模块与模块、模块与基座通信 (重要)
    模块测试 (重要)
    模块复用基座拦截器
    模块复用基座数据源
    基座与模块间类委托加载原理介绍



    -

    最后修改 November 3, 2023: Revert "update docs" (d7d8e5c4)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    编码规范

    + + +

    基础规范

    +
      +
    1. SOFAServerless 模块中官方验证并兼容的中间件客户端列表详见此处。基座中可以使用任意中间件客户端。
    2. +
    3. 如果使用了模块热卸载能力,模块自定义的 Timer、ThreadPool 等需要在模块卸载时手动清理。您可以监听 Spring 的 ContextClosedEvent 事件,在事件处理函数中清理必要资源,也可以在 Spring XML 定义 Timer、ThreadPool 的地方指定它们的 destroy-method,在模块卸载时,Spring 会自动执行 destroy-method
    4. +
    5. 基座启动时会部署所有模块,所以基座编码时,一定要向所有模块兼容,否则基座会发布失败。如果遇到无法绕过的不兼容变更(一般是在模块拆分过程中会有比较多的基座与模块不兼容变更),请参见基座与模块不兼容发布
    6. +
    +

    知识点

    +

    模块瘦身 (重要)
    +模块与模块、模块与基座通信 (重要)
    +模块测试 (重要)
    +模块复用基座拦截器
    +模块复用基座数据源
    +基座与模块间类委托加载原理介绍

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 3, 2023: Revert "update docs" (d7d8e5c4) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/index.html b/docs/public/docs/tutorials/module-development/index.html index 058a4ffc4..2a95a58d0 100644 --- a/docs/public/docs/tutorials/module-development/index.html +++ b/docs/public/docs/tutorials/module-development/index.html @@ -1,12 +1,574 @@ -模块研发 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +模块研发 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    模块研发

    -

    最后修改 November 14, 2023: add build and deploy (1950dff0)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块研发

    + + + +
    + + + + + + + + +
    + + +
    +
    + 编码规范 +
    +

    +
    + + +
    +
    + 模块瘦身 +
    +

    +
    + + +
    +
    + 模块与模块、模块与基座通信 +
    +

    +
    + + +
    +
    + 模块本地开发 +
    +

    +
    + + +
    +
    + 模块测试 +
    +

    +
    + + +
    +
    + 复用基座拦截器 +
    +

    +
    + + +
    +
    + 复用基座数据源 +
    +

    +
    + + +
    +
    + 静态合并部署 +
    +

    +
    + + +
    +
    + 模块中官方支持的中间件客户端 +
    +

    +
    + + +
    +
    + SOFAArk 关键用户文档 +
    +

    +
    + + +
    +
    + +
    +

    +
    + + +
    + +
    + + + + + + +
    + +
    +
    + 最后修改 November 14, 2023: add build and deploy (1950dff0) +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/module-and-base-communication/index.html b/docs/public/docs/tutorials/module-development/module-and-base-communication/index.html index 51244c4af..06915f029 100644 --- a/docs/public/docs/tutorials/module-development/module-and-base-communication/index.html +++ b/docs/public/docs/tutorials/module-development/module-and-base-communication/index.html @@ -1,144 +1,647 @@ -模块与模块、模块与基座通信 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块与模块、模块与基座通信 | SOFAServerless + + + + + + + + + + + + + + - - +@RestController public class SampleController { @RequestMapping(value = "/", method = RequestMethod.GET) public String hello() { Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "studentProvider", Provider.class); Result result = studentProvider.provide(new Param()); Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class); Result result1 = teacherProvider.provide(new Param()); Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class); for (String beanName : providerMap.keySet()) { Result result2 = providerMap.get(beanName).provide(new Param()); } return "hello to ark master biz"; } } 模块调用基座 方式一:注解 @AutowiredFromBase @RestController public class SampleController { @AutowiredFromBase(name = "sampleServiceImplNew") private SampleService sampleServiceImplNew; @AutowiredFromBase(name = "sampleServiceImpl") private SampleService sampleServiceImpl; @AutowiredFromBase private List<SampleService> sampleServiceList; @AutowiredFromBase private Map<String, SampleService> sampleServiceMap; @AutowiredFromBase private AppService appService; @RequestMapping(value = "/", method = RequestMethod."/> + + + + + + + + + + + + + + + + + + + -

    模块与模块、模块与基座通信

    基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 -当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

    Spring 环境

    基座调用模块

    只能使用 SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    -                "studentProvider", Provider.class);
    -        Result result = studentProvider.provide(new Param());
    -
    -        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    -                "teacherProvider", Provider.class);
    -        Result result1 = teacherProvider.provide(new Param());
    -        
    -        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
    -                Provider.class);
    -        for (String beanName : providerMap.keySet()) {
    -            Result result2 = providerMap.get(beanName).provide(new Param());
    -        }
    -
    -        return "hello to ark master biz";
    -    }
    -}
    -

    模块调用基座

    方式一:注解 @AutowiredFromBase

    @RestController
    -public class SampleController {
    -
    -    @AutowiredFromBase(name = "sampleServiceImplNew")
    -    private SampleService sampleServiceImplNew;
    -
    -    @AutowiredFromBase(name = "sampleServiceImpl")
    -    private SampleService sampleServiceImpl;
    -
    -    @AutowiredFromBase
    -    private List<SampleService> sampleServiceList;
    -
    -    @AutowiredFromBase
    -    private Map<String, SampleService> sampleServiceMap;
    -
    -    @AutowiredFromBase
    -    private AppService appService;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        sampleServiceImplNew.service();
    -
    -        sampleServiceImpl.service();
    -
    -        for (SampleService sampleService : sampleServiceList) {
    -            sampleService.service();
    -        }
    -
    -        for (String beanName : sampleServiceMap.keySet()) {
    -            sampleServiceMap.get(beanName).service();
    -        }
    -
    -        appService.getAppName();
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    方式二:编程API SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
    -        String result = sampleServiceImplFromFinder.service();
    -        System.out.println(result);
    -
    -        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
    -        for (String beanName : sampleServiceMapFromFinder.keySet()) {
    -            String result1 = sampleServiceMapFromFinder.get(beanName).service();
    -            System.out.println(result1);
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    模块调用模块

    参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

    方式一:注解 @AutowiredFromBiz

    @RestController
    -public class SampleController {
    -
    -    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    -    private Provider studentProvider;
    -
    -    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    -    private Provider teacherProvider;
    -
    -    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    -    private List<Provider> providers;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Result provide = studentProvider.provide(new Param());
    -
    -        Result provide1 = teacherProvider.provide(new Param());
    -
    -        for (Provider provider : providers) {
    -            Result provide2 = provider.provide(new Param());
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    方式二:编程API SpringServiceFinder

    @RestController
    -public class SampleController {
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -
    -        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
    -        Result result1 = teacherProvider1.provide(new Param());
    -
    -        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
    -        for (String beanName : providerMap.keySet()) {
    -            Result result2 = providerMap.get(beanName).provide(new Param());
    -        }
    -
    -        return "hello to ark2 dynamic deploy";
    -    }
    -}
    -

    完整样例

    SOFABoot 环境

    请参考该文档



    -

    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块与模块、模块与基座通信

    + + +

    基座与模块之间、模块与模块之间存在 spring 上下文隔离,互相的 bean 不会冲突、不可见。然而很多应用场景比如中台模式、独立模块模式等存在基座调用模块、模块调用基座、模块与模块互相调用的场景。 +当前支持3种方式调用,@AutowiredFromBiz, @AutowiredFromBase, SpringServiceFinder 方法调用,注意三个方式使用的情况不同。

    +

    Spring 环境

    +

    基座调用模块

    +

    只能使用 SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Provider studentProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    +                "studentProvider", Provider.class);
    +        Result result = studentProvider.provide(new Param());
    +
    +        Provider teacherProvider = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT",
    +                "teacherProvider", Provider.class);
    +        Result result1 = teacherProvider.provide(new Param());
    +        
    +        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT",
    +                Provider.class);
    +        for (String beanName : providerMap.keySet()) {
    +            Result result2 = providerMap.get(beanName).provide(new Param());
    +        }
    +
    +        return "hello to ark master biz";
    +    }
    +}
    +

    模块调用基座

    +

    方式一:注解 @AutowiredFromBase

    +
    @RestController
    +public class SampleController {
    +
    +    @AutowiredFromBase(name = "sampleServiceImplNew")
    +    private SampleService sampleServiceImplNew;
    +
    +    @AutowiredFromBase(name = "sampleServiceImpl")
    +    private SampleService sampleServiceImpl;
    +
    +    @AutowiredFromBase
    +    private List<SampleService> sampleServiceList;
    +
    +    @AutowiredFromBase
    +    private Map<String, SampleService> sampleServiceMap;
    +
    +    @AutowiredFromBase
    +    private AppService appService;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        sampleServiceImplNew.service();
    +
    +        sampleServiceImpl.service();
    +
    +        for (SampleService sampleService : sampleServiceList) {
    +            sampleService.service();
    +        }
    +
    +        for (String beanName : sampleServiceMap.keySet()) {
    +            sampleServiceMap.get(beanName).service();
    +        }
    +
    +        appService.getAppName();
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    方式二:编程API SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        SampleService sampleServiceImplFromFinder = SpringServiceFinder.getBaseService("sampleServiceImpl", SampleService.class);
    +        String result = sampleServiceImplFromFinder.service();
    +        System.out.println(result);
    +
    +        Map<String, SampleService> sampleServiceMapFromFinder = SpringServiceFinder.listBaseServices(SampleService.class);
    +        for (String beanName : sampleServiceMapFromFinder.keySet()) {
    +            String result1 = sampleServiceMapFromFinder.get(beanName).service();
    +            System.out.println(result1);
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    模块调用模块

    +

    参考模块调用基座,注解使用 @AutowiredFromBiz 和 编程API支持 SpringServiceFinder。

    +

    方式一:注解 @AutowiredFromBiz

    +
    @RestController
    +public class SampleController {
    +
    +    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT", name = "studentProvider")
    +    private Provider studentProvider;
    +
    +    @AutowiredFromBiz(bizName = "biz", name = "teacherProvider")
    +    private Provider teacherProvider;
    +
    +    @AutowiredFromBiz(bizName = "biz", bizVersion = "0.0.1-SNAPSHOT")
    +    private List<Provider> providers;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Result provide = studentProvider.provide(new Param());
    +
    +        Result provide1 = teacherProvider.provide(new Param());
    +
    +        for (Provider provider : providers) {
    +            Result provide2 = provider.provide(new Param());
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    方式二:编程API SpringServiceFinder

    +
    @RestController
    +public class SampleController {
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +
    +        Provider teacherProvider1 = SpringServiceFinder.getModuleService("biz", "0.0.1-SNAPSHOT", "teacherProvider", Provider.class);
    +        Result result1 = teacherProvider1.provide(new Param());
    +
    +        Map<String, Provider> providerMap = SpringServiceFinder.listModuleServices("biz", "0.0.1-SNAPSHOT", Provider.class);
    +        for (String beanName : providerMap.keySet()) {
    +            Result result2 = providerMap.get(beanName).provide(new Param());
    +        }
    +
    +        return "hello to ark2 dynamic deploy";
    +    }
    +}
    +

    完整样例

    +

    SOFABoot 环境

    +

    请参考该文档

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 9, 2023: add notication for modules communication (5b4aa8ef) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/module-debug/index.html b/docs/public/docs/tutorials/module-development/module-debug/index.html index f46730365..280fc2548 100644 --- a/docs/public/docs/tutorials/module-development/module-debug/index.html +++ b/docs/public/docs/tutorials/module-development/module-debug/index.html @@ -1,139 +1,651 @@ -模式测试 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块测试 | SOFAServerless + + + + + + + + + + + + + + - - +{ "code":"SUCCESS", "data":{ "bizInfos":[ { "bizName":"dynamic-provider", "bizState":"ACTIVATED", "bizVersion":"1.0.0", "declaredMode":true, "identity":"dynamic-provider:1.0.0", "mainClass":"io.sofastack.dynamic.provider.ProviderApplication", "priority":100, "webContextPath":"provider" } ], "code":"SUCCESS", "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769" } } 部署失败返回结果样例:"/> + + + + + + + + + + + + + + + + + + + -

    模式测试

    本地调试

    您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
    Arklet HTTP 接口主要提供了以下能力:

    1. 部署和卸载模块。
    2. 查询所有已部署的模块信息。
    3. 查询各项系统和业务指标。

    部署模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
    -

    请求体样例:

    {
    -    "bizName": "test",
    -    "bizVersion": "1.0.0",
    -    // local path should start with file://, alse support remote url which can be downloaded
    -    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
    -}
    -

    部署成功返回结果样例:

    {
    -  "code":"SUCCESS",
    -  "data":{
    -    "bizInfos":[
    -      {
    -        "bizName":"dynamic-provider",
    -        "bizState":"ACTIVATED",
    -        "bizVersion":"1.0.0",
    -        "declaredMode":true,
    -        "identity":"dynamic-provider:1.0.0",
    -        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    -        "priority":100,
    -        "webContextPath":"provider"
    -      }
    -    ],
    -    "code":"SUCCESS",
    -    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
    -  }
    -}
    -

    部署失败返回结果样例:

    {
    -  "code":"FAILED",
    -  "data":{
    -    "code":"REPEAT_BIZ",
    -    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
    -  }
    -}
    -

    卸载模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
    -

    请求体样例:

    {
    -    "bizName":"dynamic-provider",
    -    "bizVersion":"1.0.0"
    -}
    -

    卸载成功返回结果样例:

    {
    -  "code":"SUCCESS"
    -}
    -

    卸载失败返回结果样例:

    {
    -  "code":"FAILED",
    -  "data":{
    -    "code":"NOT_FOUND_BIZ",
    -    "message":"Uninstall biz: test:1.0.0 not found."
    -  }
    -}
    -

    查询模块

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
    -

    请求体样例:

    {}
    -

    返回结果样例:

    {
    -  "code":"SUCCESS",
    -  "data":[
    -    {
    -      "bizName":"dynamic-provider",
    -      "bizState":"ACTIVATED",
    -      "bizVersion":"1.0.0",
    -      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    -      "webContextPath":"provider"
    -    },
    -    {
    -      "bizName":"stock-mng",
    -      "bizState":"ACTIVATED",
    -      "bizVersion":"1.0.0",
    -      "mainClass":"embed main",
    -      "webContextPath":"/"
    -    }
    -  ]
    -}
    -

    获取帮助

    Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
    -

    请求体样例:

    {}
    -

    返回结果样例:

    {
    -    "code":"SUCCESS",
    -    "data":[
    -        {
    -            "desc":"query all ark biz(including master biz)",
    -            "id":"queryAllBiz"
    -        },
    -        {
    -            "desc":"list all supported commands",
    -            "id":"help"
    -        },
    -        {
    -            "desc":"uninstall one ark biz",
    -            "id":"uninstallBiz"
    -        },
    -        {
    -            "desc":"switch one ark biz",
    -            "id":"switchBiz"
    -        },
    -        {
    -            "desc":"install one ark biz",
    -            "id":"installBiz"
    -        }
    -    ]
    -}
    -

    本地构建如何不改变模块版本号

    添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

    <profile>
    -    <id>local</id>
    -    <build>
    -        <plugins>
    -            <plugin>
    -                <groupId>com.alipay.sofa</groupId>
    -                <artifactId>sofa-ark-maven-plugin</artifactId>
    -                <configuration>
    -                    <finalName>${project.artifactId}-${project.version}</finalName>
    -                    <bizVersion>${project.version}</bizVersion>
    -                </configuration>
    -            </plugin>
    -        </plugins>
    -    </build>
    -</profile>
    -

    单元测试

    模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。



    -

    最后修改 October 26, 2023: add styles (6b806506)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块测试

    + + +

    本地调试

    +

    您可以在本地或远程先启动基座,然后使用客户端 Arklet 暴露的 HTTP 接口在本地或远程部署模块,并且可以给模块代码打断点实现模块的本地或远程 Debug。
    Arklet HTTP 接口主要提供了以下能力:

    +
      +
    1. 部署和卸载模块。
    2. +
    3. 查询所有已部署的模块信息。
    4. +
    5. 查询各项系统和业务指标。
    6. +
    +

    部署模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/installBiz 
    +

    请求体样例:

    +
    {
    +    "bizName": "test",
    +    "bizVersion": "1.0.0",
    +    // local path should start with file://, alse support remote url which can be downloaded
    +    "bizUrl": "file:///Users/jaimezhang/workspace/github/sofa-ark-dynamic-guides/dynamic-provider/target/dynamic-provider-1.0.0-ark-biz.jar"
    +}
    +

    部署成功返回结果样例:

    +
    {
    +  "code":"SUCCESS",
    +  "data":{
    +    "bizInfos":[
    +      {
    +        "bizName":"dynamic-provider",
    +        "bizState":"ACTIVATED",
    +        "bizVersion":"1.0.0",
    +        "declaredMode":true,
    +        "identity":"dynamic-provider:1.0.0",
    +        "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    +        "priority":100,
    +        "webContextPath":"provider"
    +      }
    +    ],
    +    "code":"SUCCESS",
    +    "message":"Install Biz: dynamic-provider:1.0.0 success, cost: 1092 ms, started at: 16:07:47,769"
    +  }
    +}
    +

    部署失败返回结果样例:

    +
    {
    +  "code":"FAILED",
    +  "data":{
    +    "code":"REPEAT_BIZ",
    +    "message":"Biz: dynamic-provider:1.0.0 has been installed or registered."
    +  }
    +}
    +

    卸载模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/uninstallBiz 
    +

    请求体样例:

    +
    {
    +    "bizName":"dynamic-provider",
    +    "bizVersion":"1.0.0"
    +}
    +

    卸载成功返回结果样例:

    +
    {
    +  "code":"SUCCESS"
    +}
    +

    卸载失败返回结果样例:

    +
    {
    +  "code":"FAILED",
    +  "data":{
    +    "code":"NOT_FOUND_BIZ",
    +    "message":"Uninstall biz: test:1.0.0 not found."
    +  }
    +}
    +

    查询模块

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/queryAllBiz 
    +

    请求体样例:

    +
    {}
    +

    返回结果样例:

    +
    {
    +  "code":"SUCCESS",
    +  "data":[
    +    {
    +      "bizName":"dynamic-provider",
    +      "bizState":"ACTIVATED",
    +      "bizVersion":"1.0.0",
    +      "mainClass":"io.sofastack.dynamic.provider.ProviderApplication",
    +      "webContextPath":"provider"
    +    },
    +    {
    +      "bizName":"stock-mng",
    +      "bizState":"ACTIVATED",
    +      "bizVersion":"1.0.0",
    +      "mainClass":"embed main",
    +      "webContextPath":"/"
    +    }
    +  ]
    +}
    +

    获取帮助

    +

    Arklet 暴露的所有对外 HTTP 接口,可以查看 Arklet 接口帮助:

    +
    curl -X POST -H "Content-Type: application/json" http://127.0.0.1:1238/help 
    +

    请求体样例:

    +
    {}
    +

    返回结果样例:

    +
    {
    +    "code":"SUCCESS",
    +    "data":[
    +        {
    +            "desc":"query all ark biz(including master biz)",
    +            "id":"queryAllBiz"
    +        },
    +        {
    +            "desc":"list all supported commands",
    +            "id":"help"
    +        },
    +        {
    +            "desc":"uninstall one ark biz",
    +            "id":"uninstallBiz"
    +        },
    +        {
    +            "desc":"switch one ark biz",
    +            "id":"switchBiz"
    +        },
    +        {
    +            "desc":"install one ark biz",
    +            "id":"installBiz"
    +        }
    +    ]
    +}
    +

    本地构建如何不改变模块版本号

    +

    添加以下 maven profile,本地构建模块使用命令 mvn clean package -Plocal

    +
    <profile>
    +    <id>local</id>
    +    <build>
    +        <plugins>
    +            <plugin>
    +                <groupId>com.alipay.sofa</groupId>
    +                <artifactId>sofa-ark-maven-plugin</artifactId>
    +                <configuration>
    +                    <finalName>${project.artifactId}-${project.version}</finalName>
    +                    <bizVersion>${project.version}</bizVersion>
    +                </configuration>
    +            </plugin>
    +        </plugins>
    +    </build>
    +</profile>
    +

    单元测试

    +

    模块里支持使用标准 JUnit4 和 TestNG 编写和执行单元测试。

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 December 19, 2023: TYPO FIX, "模式"->"模块" (8113003b) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/module-dev-arkctl/index.html b/docs/public/docs/tutorials/module-development/module-dev-arkctl/index.html index 3c1b3045d..0225d219a 100644 --- a/docs/public/docs/tutorials/module-development/module-dev-arkctl/index.html +++ b/docs/public/docs/tutorials/module-development/module-dev-arkctl/index.html @@ -1,61 +1,598 @@ -模式本地开发 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块本地开发 | SOFAServerless + + + + + + + + + + + + + + - - +场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。 准备:"/> + + + + + + + + + + + + + + + + + + + -

    模式本地开发

    Arkctl 工具安装

    方法一:

    1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
    2. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。

    方法二:

    1. 二进制列表 中下载对应的二进制并加入到本地 -path 中。

    本地快速部署

    你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

    场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

    准备:

    1. 在本地启动一个基座。
    2. 打开一个模块项目仓库。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

    准备:

    1. 在本地启动一个基座。
    2. 准备一个构建好的 jar 包。

    执行命令:

    arkctl deploy /path/to/your/pre/built/bundle-biz.jar
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

    准备:

    1. 在远程已经运行起来的基座 pod。
    2. 打开一个模块项目仓库。
    3. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy --pod {namespace}/{podName}
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

    准备:

    1. 在本地启动一个基座。
    2. 打开一个多模块 Maven 项目仓库。

    执行命令:

    # 需要在仓库的根目录下执行。
    -# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    -arkctl deploy --sub ./path/to/your/sub/module
    -

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    场景 5: 查询当前基座中已经部署的模块。

    准备:

    1. 在本地启动一个基座。

    执行命令:

    arkctl status
    -

    场景 6: 查询远程 k8s 环境基座中已经部署的模块。

    准备:

    1. 在远程 k8s 环境启动一个基座。
    2. 确保本地有 kube 证书以及有关权限。

    执行命令:

    arkctl status --pod {namespace}/{name}
    -
    -

    最后修改 November 15, 2023: add turitoal for kcd (acae7532)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块本地开发

    + + +

    Arkctl 工具安装

    +

    方法一:

    +
      +
    1. 本地安装 go 环境,go 依赖版本在 1.21 以上。
    2. +
    3. 执行 go install todo 独立的 arkctl go 仓库 命令,安装 arkctl 工具。
    4. +
    +

    方法二:

    +
      +
    1. 二进制列表 中下载对应的二进制并加入到本地 +path 中。
    2. +
    +

    本地快速部署

    +

    你可以使用 arkctl 工具快速地进行模块的构建和部署,提高本地调试和研发效率。

    +

    场景 1:模块 jar 包构建 + 部署到本地运行的基座中。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 打开一个模块项目仓库。
    4. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 2:部署一个本地构建好的 jar 包到本地运行的基座中。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 准备一个构建好的 jar 包。
    4. +
    +

    执行命令:

    +
    arkctl deploy /path/to/your/pre/built/bundle-biz.jar
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 3: 模块 jar 包构建 + 部署到远程运行的 k8s 基座中。

    +

    准备:

    +
      +
    1. 在远程已经运行起来的基座 pod。
    2. +
    3. 打开一个模块项目仓库。
    4. +
    5. 本地需要有具备 exec 权限的 k8s 证书以及 kubectl 命令行工具。
    6. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy --pod {namespace}/{podName}
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 4: 在多模块的 Maven 项目中,在 Root 构建并部署子模块的 jar 包。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    3. 打开一个多模块 Maven 项目仓库。
    4. +
    +

    执行命令:

    +
    # 需要在仓库的根目录下执行。
    +# 比如,如果是 maven 项目,需要在根 pom.xml 所在的目录下执行。
    +arkctl deploy --sub ./path/to/your/sub/module
    +

    命令执行完成后即部署成功,用户可以进行相关的模块功能调试验证。

    +

    场景 5: 查询当前基座中已经部署的模块。

    +

    准备:

    +
      +
    1. 在本地启动一个基座。
    2. +
    +

    执行命令:

    +
    arkctl status
    +

    场景 6: 查询远程 k8s 环境基座中已经部署的模块。

    +

    准备:

    +
      +
    1. 在远程 k8s 环境启动一个基座。
    2. +
    3. 确保本地有 kube 证书以及有关权限。
    4. +
    +

    执行命令:

    +
    arkctl status --pod {namespace}/{name}
    +
    + +
    + + + + + + +
    + + +
    +
    + 最后修改 December 19, 2023: TYPO FIX, "模式"->"模块" (8113003b) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/module-slimming/index.html b/docs/public/docs/tutorials/module-development/module-slimming/index.html index a9257e1b7..7971074b3 100644 --- a/docs/public/docs/tutorials/module-development/module-slimming/index.html +++ b/docs/public/docs/tutorials/module-development/module-slimming/index.html @@ -1,96 +1,616 @@ -模块瘦身 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块瘦身 | SOFAServerless + + + + + + + + + + + + + + - - +<!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar --> <plugin> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-maven-plugin</artifactId> <version>2.2.5</version> <executions> <execution> <id>default-cli</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <skipArkExecutable>true</skipArkExecutable> <outputDirectory>./target</outputDirectory> <bizName>biz1</bizName> <!-- packExcludesConfig 模块瘦身配置,文件名自定义,和配置对应即可--> <!-- 配置文件位置:biz1/conf/ark/rules.txt--> <packExcludesConfig>rules.txt</packExcludesConfig> <webContextPath>biz1</webContextPath> <declaredMode>true</declaredMode> <!-- 打包、安装和发布 ark biz--> <!"/> + + + + + + + + + + + + + + + + + + + -

    模块瘦身

    为什么要瘦身

    为了让模块安装更快、内存消耗更小:

    • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
    • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。

    一键自动瘦身

    瘦身原则

    构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

    步骤一

    在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

    excludeGroupIds=org.apache*
    -excludeArtifactIds=commons-lang
    -

    步骤二

    在模块打包插件中,引入上述配置文件:

        <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>2.2.5</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>biz1</bizName>
    -            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
    -            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
    -            <packExcludesConfig>rules.txt</packExcludesConfig>
    -            <webContextPath>biz1</webContextPath>
    -            <declaredMode>true</declaredMode>
    -            <!--					打包、安装和发布 ark biz-->
    -            <!--					静态合并部署需要配置-->
    -            <!--					<attach>true</attach>-->
    -        </configuration>
    -    </plugin>
    -

    步骤三

    打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

    您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

    基本原理

    SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

    1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
    2. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。

    基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
    所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

    手动排包瘦身

    模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
    所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
    image.png

    如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
    image.png
    确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
    image.png

    在 pom 中统一排包(更彻底)

    有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

    <dependency>
    -    <groupId>com.serverless.mymodule</groupId>
    -    <artifactId>mymodule-core</artifactId>
    -    <exclusions>
    -          <exclusion>
    -              <groupId>org.springframework</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -          <exclusion>
    -              <groupId>org.apache.commons</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -          <exclusion>
    -              <groupId>......</groupId>
    -              <artifactId>*</artifactId>
    -          </exclusion>
    -    </exclusions>
    -</dependency>
    -

    在 sofa-ark-maven-plugin 中指定排包

    通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

     <plugin>
    -      <groupId>com.alipay.sofa</groupId>
    -      <artifactId>sofa-ark-maven-plugin</artifactId>
    -      <executions>
    -          <execution>
    -              <id>default-cli</id>
    -              <goals>
    -                  <goal>repackage</goal>
    -              </goals>
    -          </execution>
    -      </executions>
    -      <configuration>
    -          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
    -          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
    -          <outputDirectory>../../target</outputDirectory>
    -          <bizName>mymodule</bizName>
    -          <finalName>mymodule-${project.version}-${timestamp}</finalName>
    -          <bizVersion>${project.version}-${timestamp}</bizVersion>
    -          <webContextPath>/mymodule</webContextPath>
    -      </configuration>
    -  </plugin>
    -


    -

    最后修改 November 15, 2023: update samples ark version (0dbb71a6)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块瘦身

    + + +

    为什么要瘦身

    +

    为了让模块安装更快、内存消耗更小:

    +
      +
    • 提高模块安装的速度,减少模块包大小,减少启动依赖,控制模块安装耗时 < 30秒,甚至 < 5秒。
    • +
    • 模块启动后 Spring 上下文中会创建很多对象,如果启用了模块热卸载,可能无法完全回收,安装次数过多会造成 Old 区、Metaspace 区开销大,触发频繁 FullGC,所有要控制单模块包大小 < 5MB。这样不替换或重启基座也能热部署热卸载数百次。
    • +
    +

    一键自动瘦身

    +

    瘦身原则

    +

    构建 ark-biz jar 包的原则是,在保证模块功能的前提下,将框架、中间件等通用的包尽量放置到基座中,模块中复用基座的包,这样打出的 ark-biz jar 会更加轻量。在复杂应用中,为了更好的使用模块自动瘦身功能,需要在模块瘦身配置 (模块根目录/conf/ark/文件名.txt) 中,在样例给出的配置名单的基础上,按照既定格式,排除更多的通用依赖包。

    +

    步骤一

    +

    在「模块项目根目录/conf/ark/文件名.txt」中(比如:my-module/conf/ark/rules.txt),按照如下格式配置需要下沉到基座的框架和中间件常用包。您也可以直接复制默认的 rules.txt 文件内容到您的项目中。

    +
    excludeGroupIds=org.apache*
    +excludeArtifactIds=commons-lang
    +

    步骤二

    +

    在模块打包插件中,引入上述配置文件:

    +
        <!-- 插件1:打包插件为 sofa-ark biz 打包插件,打包成 ark biz jar -->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>2.2.5</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>biz1</bizName>
    +            <!-- packExcludesConfig	模块瘦身配置,文件名自定义,和配置对应即可-->
    +            <!--					配置文件位置:biz1/conf/ark/rules.txt-->
    +            <packExcludesConfig>rules.txt</packExcludesConfig>
    +            <webContextPath>biz1</webContextPath>
    +            <declaredMode>true</declaredMode>
    +            <!--					打包、安装和发布 ark biz-->
    +            <!--					静态合并部署需要配置-->
    +            <!--					<attach>true</attach>-->
    +        </configuration>
    +    </plugin>
    +

    步骤三

    +

    打包构建出模块 ark-biz jar 包即可,您可以明显看出瘦身后的 ark-biz jar 包大小差异。

    +

    您可点击此处查看完整模块瘦身样例工程。您也可以阅读下文继续了解模块的瘦身原理。

    +

    基本原理

    +

    SOFAServerless 底层借助 SOFAArk 框架,实现了模块之间、模块和基座之间的相互隔离,以下两个核心逻辑对编码非常重要,需要深刻理解:

    +
      +
    1. 基座有独立的类加载器和 Spring 上下文,模块也有独立的类加载器和** Spring 上下文**,相互之间 Spring 上下文都是隔离的
    2. +
    3. 模块启动时会初始化各种对象,会优先使用模块的类加载器去加载构建产物 FatJar 中的 class、resource 和 Jar 包,找不到的类会委托基座的类加载器去查找。
    4. +
    +

    +

    基于这套类委托的加载机制,让基座和模块共用的 class、resource 和 Jar 包通通下沉到基座中,可以让模块构建产物非常小,更重要的是还能让模块在运行中大量复用基座已有的 class、bean、service、IO 连接池、线程池等资源,从而模块消耗的内存非常少,启动也能非常快
    所谓模块瘦身,就是让基座已经有的 Jar 依赖务必在模块中剔除干净,在主 pom.xml 和 bootstrap/pom.xml 将共用的 Jar 包 scope 都声明为 provided,让其不参与打包构建。

    +

    手动排包瘦身

    +

    模块运行时装载类时,会优先从自己的依赖里找,找不到的话再委托基座的 ClassLoader 去加载。
    所以对于基座已经存在的依赖,在模块 pom 里将其 scope 设置成 provided,避免其参与模块打包。
    image.png

    +

    如果要排除的依赖无法找到,可以利用 maven helper 插件找到其直接依赖。举个例子,图示中要排除的依赖为 spring-boot-autoconfigure,右边的直接依赖有 sofa-boot-alipay-runtime,ddcs-alipay-sofa-boot-starter等(只需要看 scope 为 compile 的依赖):
    image.png
    确定自己代码 pom.xml 中有 ddcs-alipay-sofa-boot-starter,增加 exlcusions 来排除依赖:
    image.png

    +

    在 pom 中统一排包(更彻底)

    +

    有些依赖引入了过多的间接依赖,手动排查比较困难,此时可以通过通配符匹配,把那些中间件、基座的依赖全部剔除掉,如 org.apache.commons、org.springframework 等等,这种方式会把间接依赖都排除掉,相比使用 sofa-ark-maven-plugin 排包效率会更高:

    +
    <dependency>
    +    <groupId>com.serverless.mymodule</groupId>
    +    <artifactId>mymodule-core</artifactId>
    +    <exclusions>
    +          <exclusion>
    +              <groupId>org.springframework</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +          <exclusion>
    +              <groupId>org.apache.commons</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +          <exclusion>
    +              <groupId>......</groupId>
    +              <artifactId>*</artifactId>
    +          </exclusion>
    +    </exclusions>
    +</dependency>
    +

    在 sofa-ark-maven-plugin 中指定排包

    +

    通过使用 **excludeGroupIds、excludeGroupIds **能够排除大量基座上已有的公共依赖:

    +
     <plugin>
    +      <groupId>com.alipay.sofa</groupId>
    +      <artifactId>sofa-ark-maven-plugin</artifactId>
    +      <executions>
    +          <execution>
    +              <id>default-cli</id>
    +              <goals>
    +                  <goal>repackage</goal>
    +              </goals>
    +          </execution>
    +      </executions>
    +      <configuration>
    +          <excludeGroupIds>io.netty,org.apache.commons,......</excludeGroupIds>
    +          <excludeArtifactIds>validation-api,fastjson,hessian,slf4j-api,junit,velocity,......</excludeArtifactIds>
    +          <outputDirectory>../../target</outputDirectory>
    +          <bizName>mymodule</bizName>
    +          <finalName>mymodule-${project.version}-${timestamp}</finalName>
    +          <bizVersion>${project.version}-${timestamp}</bizVersion>
    +          <webContextPath>/mymodule</webContextPath>
    +      </configuration>
    +  </plugin>
    +

    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 December 12, 2023: update ark to 2.2.5 (155856d0) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/reuse-base-datasource/index.html b/docs/public/docs/tutorials/module-development/reuse-base-datasource/index.html index b43be4fcf..0290cd02a 100644 --- a/docs/public/docs/tutorials/module-development/reuse-base-datasource/index.html +++ b/docs/public/docs/tutorials/module-development/reuse-base-datasource/index.html @@ -1,86 +1,576 @@ -复用基座数据源 | SOFAServerless + + + + + + + + + + + + + + + + + + +复用基座数据源 | SOFAServerless + + + + + + + + + + + + + + - - +参考 demo:/sofa-serverless/samples/springboot-samples/db/mybatis/biz1 +通过SpringBeanFinder.getBaseBean获取到基座的 Bean 对象,然后注册成模块的 Bean: +@Configuration @MapperScan(basePackages = "com.alipay.sofa.biz1.mapper", sqlSessionFactoryRef = "mysqlSqlFactory") @EnableTransactionManagement public class MybatisConfig { //tips:不要初始化一个基座的DataSource,当模块被卸载的是,基座数据源会被销毁,transactionManager,transactionTemplate,mysqlSqlFactory被销毁没有问题 @Bean(name = "transactionManager") public PlatformTransactionManager platformTransactionManager() { return (PlatformTransactionManager) getBaseBean("transactionManager"); } @Bean(name = "transactionTemplate") public TransactionTemplate transactionTemplate() { return (TransactionTemplate) getBaseBean("transactionTemplate"); } @Bean(name = "mysqlSqlFactory") public SqlSessionFactoryBean mysqlSqlFactory() throws IOException { //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法 DataSource dataSource = (DataSource) getBaseBean("dataSource"); SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean(); mysqlSqlFactory."/> + + + + + + + + + + + + + + + + + + + -

    复用基座数据源

    建议

    强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

    SpringBoot 解法

    在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
    通过BaseAppUtils.getBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

    
    -@Configuration
    -@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    -@EnableTransactionManagement
    -public class MybatisConfig {
    -
    -    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    -    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    -
    -    @Bean(name = "transactionManager")
    -    public PlatformTransactionManager platformTransactionManager() {
    -        return (PlatformTransactionManager) BaseAppUtils.getBean("transactionManager");
    -    }
    -
    -    @Bean(name = "transactionTemplate")
    -    public TransactionTemplate transactionTemplate() {
    -        return (TransactionTemplate) BaseAppUtils.getBean("transactionTemplate");
    -    }
    -
    -    @Bean(name = "mysqlSqlFactory")
    -    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    -        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    -        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBean("dataSource");
    -        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    -        mysqlSqlFactory.setDataSource(dataSource);
    -        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    -                .getResources("classpath:mapper/*.xml"));
    -        return mysqlSqlFactory;
    -    }
    -}
    -

    SOFABoot 解法

    如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
    如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
    image.png

    
    -@Configuration
    -@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    -@EnableTransactionManagement
    -public class MybatisConfig {
    -
    -    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    -    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    -    
    -    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
    -
    -    @Bean(name = "transactionManager")
    -    public PlatformTransactionManager platformTransactionManager() {
    -        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    -    }
    -
    -    @Bean(name = "transactionTemplate")
    -    public TransactionTemplate transactionTemplate() {
    -        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    -    }
    -
    -    @Bean(name = "mysqlSqlFactory")
    -    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    -        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    -        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
    -        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    -        mysqlSqlFactory.setDataSource(dataSource);
    -        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    -                .getResources("classpath:mapper/*.xml"));
    -        return mysqlSqlFactory;
    -    }
    -}
    -


    -

    最后修改 October 30, 2023: update home (f751b32b)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    复用基座数据源

    + + +

    建议

    +

    强烈建议使用本文档方式,在模块中尽可能复用基座数据源,否则模块反复部署就会反复创建、消耗数据源连接,导致模块发布运维会变慢,同时也会额外消耗内存。

    +

    SpringBoot 解法

    +

    在模块的代码中写个 MybatisConfig 类即可,这样事务模板都是复用基座的,只有 Mybatis 的 SqlSessionFactoryBean 需要新创建。
    参考 demo:/sofa-serverless/samples/springboot-samples/db/mybatis/biz1

    +

    通过SpringBeanFinder.getBaseBean获取到基座的 Bean 对象,然后注册成模块的 Bean:

    +
    
    +@Configuration
    +@MapperScan(basePackages = "com.alipay.sofa.biz1.mapper", sqlSessionFactoryRef = "mysqlSqlFactory")
    +@EnableTransactionManagement
    +public class MybatisConfig {
    +
    +    //tips:不要初始化一个基座的DataSource,当模块被卸载的是,基座数据源会被销毁,transactionManager,transactionTemplate,mysqlSqlFactory被销毁没有问题
    +
    +    @Bean(name = "transactionManager")
    +    public PlatformTransactionManager platformTransactionManager() {
    +        return (PlatformTransactionManager) getBaseBean("transactionManager");
    +    }
    +
    +    @Bean(name = "transactionTemplate")
    +    public TransactionTemplate transactionTemplate() {
    +        return (TransactionTemplate) getBaseBean("transactionTemplate");
    +    }
    +
    +    @Bean(name = "mysqlSqlFactory")
    +    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    +        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    +
    +        DataSource dataSource = (DataSource) getBaseBean("dataSource");
    +        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    +        mysqlSqlFactory.setDataSource(dataSource);
    +        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    +                .getResources("classpath:mappers/*.xml"));
    +        return mysqlSqlFactory;
    +    }
    +}
    +

    SOFABoot 解法

    +

    如果 SOFABoot 基座没有开启多 bundle(Package 里没有 MANIFEST.MF 文件),则解法和上文 SpringBoot 完全一致。
    如果有 MANIFEST.MF 文件,需要调用BaseAppUtils.getBeanOfBundle获取基座的 Bean,其中BASE_DAL_BUNDLE_NAME 为 MANIFEST.MF 里面的Module-Name
    image.png

    +
    
    +@Configuration
    +@MapperScan(basePackages = "com.alipay.serverless.dal.dao", sqlSessionFactoryRef = "mysqlSqlFactory")
    +@EnableTransactionManagement
    +public class MybatisConfig {
    +
    +    // 注意:不要初始化一个基座的 DataSource,会导致模块被热卸载的时候,基座的数据源被销毁,不符合预期。
    +    // 但是 transactionManager,transactionTemplate,mysqlSqlFactory 这些资源被销毁没有问题
    +    
    +    private static final String BASE_DAL_BUNDLE_NAME = "com.alipay.serverless.dal"
    +
    +    @Bean(name = "transactionManager")
    +    public PlatformTransactionManager platformTransactionManager() {
    +        return (PlatformTransactionManager) BaseAppUtils.getBeanOfBundle("transactionManager",BASE_DAL_BUNDLE_NAME);
    +    }
    +
    +    @Bean(name = "transactionTemplate")
    +    public TransactionTemplate transactionTemplate() {
    +        return (TransactionTemplate) BaseAppUtils.getBeanOfBundle("transactionTemplate",BASE_DAL_BUNDLE_NAME);
    +    }
    +
    +    @Bean(name = "mysqlSqlFactory")
    +    public SqlSessionFactoryBean mysqlSqlFactory() throws IOException {
    +        //数据源不能申明成模块spring上下文中的bean,因为模块卸载时会触发close方法
    +        ZdalDataSource dataSource = (ZdalDataSource) BaseAppUtils.getBeanOfBundle("dataSource",BASE_DAL_BUNDLE_NAME);
    +        SqlSessionFactoryBean mysqlSqlFactory = new SqlSessionFactoryBean();
    +        mysqlSqlFactory.setDataSource(dataSource);
    +        mysqlSqlFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
    +                .getResources("classpath:mapper/*.xml"));
    +        return mysqlSqlFactory;
    +    }
    +}
    +

    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 December 8, 2023: adjust reuse-base-datasource.md (6c47670a) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/reuse-base-interceptor/index.html b/docs/public/docs/tutorials/module-development/reuse-base-interceptor/index.html index 6c359fe94..b1ce759e3 100644 --- a/docs/public/docs/tutorials/module-development/reuse-base-interceptor/index.html +++ b/docs/public/docs/tutorials/module-development/reuse-base-interceptor/index.html @@ -1,85 +1,584 @@ -复用基座拦截器 | SOFAServerless + + + + + + + + + + + + + + + + + + +复用基座拦截器 | SOFAServerless + + + + + + + + + + + + + + - - +@Service @SofaService(uniqueId = "facadeAroundHandler") public class FacadeAroundHandler implements AnnotionService { private final static Logger LOG = LoggerConst.MY_LOGGER; public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { log.info("开始执行") joinPoint.proceed(); log.info("执行完成") } } 步骤 3: 在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。"/> + + + + + + + + + + + + + + + + + + + -

    复用基座拦截器

    诉求

    基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

    解法

    为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

    步骤 1:

    基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

    public interface AnnotionService {
    -    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
    -}
    -

    步骤 2:

    在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

    @Service
    -@SofaService(uniqueId = "facadeAroundHandler")
    -public class FacadeAroundHandler implements AnnotionService {
    -
    -    private final static Logger LOG = LoggerConst.MY_LOGGER;
    -
    -    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    -        log.info("开始执行")
    -        joinPoint.proceed();
    -        log.info("执行完成")
    -    }
    -}
    -

    步骤 3:

    在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
    注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

    //注意,这里不必申明成一个bean,不要加@Component或者@Service
    -@Aspect
    -public class FacadeAroundAspect {
    -
    -    @SofaReference(uniqueId = "facadeAroundHandler")
    -    private AnnotionService facadeAroundHandler;
    -
    -    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    -    public void facadeAroundPointcut() {
    -    }
    -
    -    @Around("facadeAroundPointcut()")
    -    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    -        return facadeAroundHandler.doAround(joinPoint);
    -    }
    -}
    -

    步骤 4:

    使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
    注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

    @Configuration
    -public class MngAspectConfiguration {
    -    @Bean
    -    public FacadeAroundAspect facadeAroundAspect() {
    -        return new FacadeAroundAspect();
    -    }
    -    @Bean
    -    public EnvRouteAspect envRouteAspect() {
    -        return new EnvRouteAspect();
    -    }
    -    @Bean
    -    public FacadeAroundAspect facadeAroundAspect() {
    -        return new FacadeAroundAspect();
    -    }
    -    
    -}
    -

    步骤 5:

    模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

    @SpringBootApplication
    -@ImportResource("classpath*:META-INF/spring/*.xml")
    -@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
    -public class ModuleBootstrapApplication {
    -    public static void main(String[] args) {
    -        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
    -        	.web(WebApplicationType.NONE);
    -        builder.build().run(args);
    -    }
    -}
    -


    -

    最后修改 October 30, 2023: update home (f751b32b)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    复用基座拦截器

    + + +

    诉求

    +

    基座中会定义很多 Aspect 切面(Spring 拦截器),你可能希望复用到模块中,但是模块和基座的 Spring 上下文是隔离的,就导致 Aspect 切面不会在模块中生效。

    +

    解法

    +

    为原有的切面类创建一个代理对象,让模块能调用到这个代理对象,然后模块通过 AutoConfiguration 注解初始化出这个代理对象。完整步骤和示例代码如下:

    +

    步骤 1:

    +

    基座代码定义一个接口,定义切面的执行方法。这个接口需要对模块可见(在模块里引用相关依赖):

    +
    public interface AnnotionService {
    +    Object doAround(ProceedingJoinPoint joinPoint) throws Throwable;
    +}
    +

    步骤 2:

    +

    在基座中编写切面的具体实现,这个实现类需要加上 @SofaService 注解(SOFABoot)或者 @SpringService 注解(SpringBoot,建设中):

    +
    @Service
    +@SofaService(uniqueId = "facadeAroundHandler")
    +public class FacadeAroundHandler implements AnnotionService {
    +
    +    private final static Logger LOG = LoggerConst.MY_LOGGER;
    +
    +    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    +        log.info("开始执行")
    +        joinPoint.proceed();
    +        log.info("执行完成")
    +    }
    +}
    +

    步骤 3:

    +

    在模块里使用 @Aspect 注解实现一个 Aspect,SOFABoot 通过 @SofaReference 注入基座上的 FacadeAroundHandler。
    注意:这里不要声明成一个 Bean,不要加 @Component 或者 @Service 注解,主需要 @Aspect 注解。

    +
    //注意,这里不必申明成一个bean,不要加@Component或者@Service
    +@Aspect
    +public class FacadeAroundAspect {
    +
    +    @SofaReference(uniqueId = "facadeAroundHandler")
    +    private AnnotionService facadeAroundHandler;
    +
    +    @Pointcut("@annotation(com.alipay.linglongmng.presentation.mvc.interceptor.FacadeAround)")
    +    public void facadeAroundPointcut() {
    +    }
    +
    +    @Around("facadeAroundPointcut()")
    +    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    +        return facadeAroundHandler.doAround(joinPoint);
    +    }
    +}
    +

    步骤 4:

    +

    使用 @Configuration 注解写个 Configuration 配置类,把模块需要的 aspectj 对象都声明成 Spring Bean。
    注意:这个 Configuration 类需要对模块可见,相关 Spring Jar 依赖需要以 provided 方式引进来。

    +
    @Configuration
    +public class MngAspectConfiguration {
    +    @Bean
    +    public FacadeAroundAspect facadeAroundAspect() {
    +        return new FacadeAroundAspect();
    +    }
    +    @Bean
    +    public EnvRouteAspect envRouteAspect() {
    +        return new EnvRouteAspect();
    +    }
    +    @Bean
    +    public FacadeAroundAspect facadeAroundAspect() {
    +        return new FacadeAroundAspect();
    +    }
    +    
    +}
    +

    步骤 5:

    +

    模块代码中显示的依赖步骤 4 创建的 Configuration 配置类 MngAspectConfiguration。

    +
    @SpringBootApplication
    +@ImportResource("classpath*:META-INF/spring/*.xml")
    +@ImportAutoConfiguration(value = {MngAspectConfiguration.class})
    +public class ModuleBootstrapApplication {
    +    public static void main(String[] args) {
    +        SpringApplicationBuilder builder = new SpringApplicationBuilder(ModuleBootstrapApplication.class)
    +        	.web(WebApplicationType.NONE);
    +        builder.build().run(args);
    +    }
    +}
    +

    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 October 30, 2023: update home (f751b32b) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/runtime-compatibility-list/index.html b/docs/public/docs/tutorials/module-development/runtime-compatibility-list/index.html index 938f63389..cf7c402f3 100644 --- a/docs/public/docs/tutorials/module-development/runtime-compatibility-list/index.html +++ b/docs/public/docs/tutorials/module-development/runtime-compatibility-list/index.html @@ -1,46 +1,688 @@ -模块中官方支持的中间件客户端 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块中官方支持的中间件客户端 | SOFAServerless + + + + + + + + + + + + + + - - +需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数 log4j2 任意 已经支持。在基座和模块引入 log4j2,并额外引入依赖:<dependency> <groupId>com.alipay.sofa.serverless</groupId> <artifactId>sofa-serverless-adapter-log4j2</artifactId> <version>${最新版 SOFAServerless 版本}</version> <scope>provided</scope> <!– 模块需要 provided –> </dependency>基座和模块完整使用样例参见此处 slf4j-api 1.x 且 >= 1."/> + + + + + + + + + + + + + + + + + + + -

    模块中官方支持的中间件客户端

    在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
    注意,这里 “已经支持” 需要在基座 POM + + + +

    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块中官方支持的中间件客户端

    + + +

    在 SOFAServerless 模块中,官方目前支持并兼容常见的中间件客户端。
    注意,这里 “已经支持” 需要在基座 POM 中引入相关客户端依赖(强烈建议使用 SpringBoot Starter 方式引入相关依赖),同时在模块 POM 中也引入相关依赖并设置 * -provided* 将依赖委托给基座加载。


    中间件客户端版本号备注
    JDK8.x
    17.x
    已经支持
    SpringBoot>= 2.3.0 或 3.x已经支持
    JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
    SOFABoot>= 3.9.0 或 4.x已经支持
    JMXN/A已经支持
    需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
    log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
    <dependency>
      <groupId>com.alipay.sofa.serverless</groupId>
      <artifactId>sofa-serverless-adapter-log4j2</artifactId>
      <version>${最新版 SOFAServerless 版本}</version>
      <scope>provided</scope> <!– 模块需要 provided –>
      </dependency>
    基座和模块完整使用样例参见此处
    slf4j-api1.x 且 >= 1.7已经支持
    tomcat7.x、8.x、9.x
    及以上均可; 10.x 暂不支持
    已经支持
    netty4.x已经支持
    sofarpc>= 5.8.6已经支持
    dubbo3.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    grpc1.x 且 >= 1.42已经支持
    基座和模块完整使用样例及注意事项可参见此处
    protobuf-java3.x 且 >= 3.17已经支持
    基座和模块完整使用样例及注意事项可参见此处
    apollo1.x 且 >= 1.6.0已经支持
    基座和模块完整使用样例及注意事项可参见此处
    kafka-client>= 2.8.0 或
    >= 3.4.0
    已经支持
    基座和模块完整使用样例可参见此处
    rocketmq4.x 且 >= 4.3.0已经支持
    基座和模块完整使用样例可参见此处
    jedis3.x已经支持
    基座和模块完整使用样例可参见此处
    xxl-job2.x 且 >= 2.1.0已经支持
    需要在模块里声明为 compile 依赖独立使用
    mybatis>= 2.2.2 或
    >= 3.5.12
    已经支持
    基座和模块完整使用样例可参见此处
    druid1.x已经支持
    基座和模块完整使用样例可参见此处
    mysql-connector-java8.x已经支持
    基座和模块完整使用样例可参见此处
    postgresql42.x 且 >= 42.3.8已经支持
    mongodb4.6.1已经支持
    基座和模块完整使用样例可参见此处
    hibernate5.x 且 >= 5.6.15已经支持
    j2cache任意已经支持
    需要在模块里声明为 compile 依赖独立使用
    opentracing0.x 且 >= 0.32.0已经支持
    elasticsearch7.x 且 >= 7.6.2已经支持
    jaspyt1.x 且 >= 1.9.3治理进行中
    OKHttp-已经支持
    需要放在基座里,请使用模块自动瘦身能力
    io.kubernetes:client10.x 且 >= 10.0.0已经支持
    net.java.dev.jna5.x 且 >= 5.12.1已经支持
    -

    最后修改 November 23, 2023: update docs (940d6393)
    - - \ No newline at end of file +provided* 将依赖委托给基座加载。

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    中间件客户端版本号备注
    JDK8.x
    17.x
    已经支持
    SpringBoot>= 2.3.0 或 3.x已经支持
    JDK17 + SpringBoot3.x 基座和模块完整使用样例可参见此处
    SpringBoot Cloud>= 2.7.x已经支持
    基座和模块完整使用样例可参见此处
    SOFABoot>= 3.9.0 或 4.x已经支持
    JMXN/A已经支持
    需要给基座加 -Dspring.jmx.default-domain=${spring.application.name} 启动参数
    log4j2任意已经支持。在基座和模块引入 log4j2,并额外引入依赖:
    <dependency>
      <groupId>com.alipay.sofa.serverless</groupId>
      <artifactId>sofa-serverless-adapter-log4j2</artifactId>
      <version>${最新版 SOFAServerless 版本}</version>
      <scope>provided</scope> <!– 模块需要 provided –>
      </dependency>
    基座和模块完整使用样例参见此处
    slf4j-api1.x 且 >= 1.7已经支持
    tomcat7.x、8.x、9.x、10.x
    及以上均可
    已经支持
    基座和模块完整使用样例可参见此处
    netty4.x已经支持
    基座和模块完整使用样例可参见此处
    sofarpc>= 5.8.6已经支持
    dubbo3.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    grpc1.x 且 >= 1.42已经支持
    基座和模块完整使用样例及注意事项可参见此处
    protobuf-java3.x 且 >= 3.17已经支持
    基座和模块完整使用样例及注意事项可参见此处
    apollo1.x 且 >= 1.6.0已经支持
    基座和模块完整使用样例及注意事项可参见此处
    nacos2.1.x已经支持
    基座和模块完整使用样例及注意事项可参见此处
    kafka-client>= 2.8.0 或
    >= 3.4.0
    已经支持
    基座和模块完整使用样例可参见此处
    rocketmq4.x 且 >= 4.3.0已经支持
    基座和模块完整使用样例可参见此处
    jedis3.x已经支持
    基座和模块完整使用样例可参见此处
    xxl-job2.x 且 >= 2.1.0已经支持
    需要在模块里声明为 compile 依赖独立使用
    mybatis>= 2.2.2 或
    >= 3.5.12
    已经支持
    基座和模块完整使用样例可参见此处
    druid1.x已经支持
    基座和模块完整使用样例可参见此处
    mysql-connector-java8.x已经支持
    基座和模块完整使用样例可参见此处
    postgresql42.x 且 >= 42.3.8已经支持
    mongodb4.6.1已经支持
    基座和模块完整使用样例可参见此处
    hibernate5.x 且 >= 5.6.15已经支持
    j2cache任意已经支持
    需要在模块里声明为 compile 依赖独立使用
    opentracing0.x 且 >= 0.32.0已经支持
    elasticsearch7.x 且 >= 7.6.2已经支持
    jaspyt1.x 且 >= 1.9.3已经支持
    OKHttp-已经支持
    需要放在基座里,请使用模块自动瘦身能力
    io.kubernetes:client10.x 且 >= 10.0.0已经支持
    net.java.dev.jna5.x 且 >= 5.12.1已经支持
    prometheus-待验证支持
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 December 13, 2023: update docs (337c9106) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/runtime-service-route/index.html b/docs/public/docs/tutorials/module-development/runtime-service-route/index.html new file mode 100644 index 000000000..fe340268e --- /dev/null +++ b/docs/public/docs/tutorials/module-development/runtime-service-route/index.html @@ -0,0 +1,480 @@ + + + + + + + + + + + + + + + + + + + + +SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    + + + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 30, 2023: add docs runtime-service-route.md (52a6fda7) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/sofa-ark/index.html b/docs/public/docs/tutorials/module-development/sofa-ark/index.html index 25cb1bf73..3a53fab25 100644 --- a/docs/public/docs/tutorials/module-development/sofa-ark/index.html +++ b/docs/public/docs/tutorials/module-development/sofa-ark/index.html @@ -1,20 +1,493 @@ -SOFAArk 关键用户文档 | SOFAServerless + + + + + + + + + + + + + + + + + + +SOFAArk 关键用户文档 | SOFAServerless + + + + + + + + + + + + + + - - +Ark 自身日志"/> + + + + + + + + + + + + + + + + + + + -

    SOFAArk 关键用户文档

    模块生命周期

    Ark 事件机制

    Ark 自身日志



    -

    最后修改 September 22, 2023: add docs (efcf6b56)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    SOFAArk 关键用户文档

    + + +

    模块生命周期

    +Ark 事件机制

    +Ark 自身日志

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 September 22, 2023: add docs (efcf6b56) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-development/static-merge-deployment/index.html b/docs/public/docs/tutorials/module-development/static-merge-deployment/index.html index c514efee5..9c7a3b40c 100644 --- a/docs/public/docs/tutorials/module-development/static-merge-deployment/index.html +++ b/docs/public/docs/tutorials/module-development/static-merge-deployment/index.html @@ -1,49 +1,542 @@ -静态合并部署 | SOFAServerless + + + + + + + + + + + + + + + + + + +静态合并部署 | SOFAServerless + + + + + + + + + + + + + + - - +<build> <plugin> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-maven-plugin</artifactId> <version>${sofa.ark.version}</version> <executions> <execution> <id>default-cli</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </build> 步骤 2:将上述 jar 包移动到指定目录。 把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/ +mv /path/to/your/biz/jar /home/sofa-ark/biz/ 步骤 3:启动基座,并通过 -D 参指定 biz 目录 java -jar -Dcom."/> + + + + + + + + + + + + + + + + + + + -

    静态合并部署

    介绍

    SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 + + + +

    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    静态合并部署

    + + +

    介绍

    +

    SOFAArk 提供了静态合并部署能力,在开发阶段,应用可以将其他应用构建成的 Biz 包(模块应用),并被最终的 Base 包(基座应用) 加载。 用户可以把 Biz 包统一放置在某个目录中,然后通过启动参数告知基座扫描这个目录,以此完成静态合并部署(详情见下描述)。如此,开发不需要考虑相互之间依赖冲突问题,Biz 之间则通过 @SofaService 和 @SofaReference 发布/引用 JVM 服务(SOFABoot,SpringBoot 还在建设中 -)进行交互。

    步骤 1:模块应用打包成 Ark Biz

    如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz -包,详见 Ark Biz 介绍。 Ark Biz 包使用 -Maven 插件 sofa-ark-maven-plugin 打包生成。

    
    -<build>
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <version>${sofa.ark.version}</version>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -    </plugin>
    -</build>
    -

    步骤 2:将上述 jar 包移动到指定目录。

    把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

    mv /path/to/your/biz/jar /home/sofa-ark/biz/
    -

    步骤 3:启动基座,并通过 -D 参指定 biz 目录

    java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
    -

    步骤 4:验证 Ark Biz(模块)启动

    在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

    telnet localhost 1234
    -

    然后执行如下命令查看模块列表:

    biz -a
    -

    此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
    上述操作可以通过 SOFAArk 静态合并部署样例 -体验。



    -

    最后修改 November 23, 2023: <fix> md (3fad4c27)
    - - \ No newline at end of file +)进行交互。

    +

    步骤 1:模块应用打包成 Ark Biz

    +

    如果开发者希望自己应用的 Ark Biz 包能够被其他应用直接当成 Jar 包依赖,进而运行在同一个 SOFAArk 容器之上,那么就需要打包发布 Ark Biz +包,详见 Ark Biz 介绍。 Ark Biz 包使用 +Maven 插件 sofa-ark-maven-plugin 打包生成。

    +
    
    +<build>
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <version>${sofa.ark.version}</version>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +    </plugin>
    +</build>
    +

    步骤 2:将上述 jar 包移动到指定目录。

    +

    把需要部署的 biz jar 都移动到指定目录,如:/home/sofa-ark/biz/

    +
    mv /path/to/your/biz/jar /home/sofa-ark/biz/
    +

    步骤 3:启动基座,并通过 -D 参指定 biz 目录

    +
    java -jar -Dcom.alipay.sofa.ark.static.biz.dir=/home/sofa-ark/biz/ sofa-ark-base.jar
    +

    步骤 4:验证 Ark Biz(模块)启动

    +

    在基座启动成功后,可以通过 telnet 启动 SOFAArk 客户端交互界面:

    +
    telnet localhost 1234
    +

    然后执行如下命令查看模块列表:

    +
    biz -a
    +

    此时应当可以看到 Master Biz(基座)和所有静态合并部署的 Ark Biz(模块)。
    +上述操作可以通过 SOFAArk 静态合并部署样例 +体验。

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 23, 2023: <fix> md (3fad4c27) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/_print/index.html b/docs/public/docs/tutorials/module-operation/_print/index.html index a5da31667..ede1c72cf 100644 --- a/docs/public/docs/tutorials/module-operation/_print/index.html +++ b/docs/public/docs/tutorials/module-operation/_print/index.html @@ -1,160 +1,745 @@ -模块运维 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +模块运维 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    1 - 模块上线与下线

    模块上线

    在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2
    -  operationStrategy:  # 此处可自定义发布运维策略
    -    upgradePolicy: installThenUninstall
    -    needConfirm: true
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy: # 此处可自定义调度策略
    -    schedulingPolicy: Scatter
    -

    ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

    模块下线

    在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

    kubectl delete yourmoduledeployment --namespace yournamespace
    -

    其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。



    2 - 模块发布

    模块发布

    修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
    -        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2
    -  operationStrategy:
    -    upgradePolicy: install_then_uninstall
    -    needConfirm: true
    -    grayTimeBetweenBatchSeconds: 0
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy:
    -    schedulingPolicy: scatter
    -

    如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

    模块回滚

    重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。



    3 - 基座和模块不兼容发布

    步骤 1

    修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

    步骤 2

    修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

    步骤 3

    使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。



    4 - 模块扩缩容与替换

    模块扩缩容

    修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
    -  operationStrategy:
    -    upgradePolicy: installThenUninstall
    -    needConfirm: true
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy: # 此处可自定义调度策略
    -    schedulingPolicy: Scatter  
    -

    如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

    模块替换

    在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

    kubectl delete yourmodule --namespace yournamespace
    -

    其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。



    5 - 模块发布运维策略

    运维策略

    为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

    字段名字段解释取值范围取值解释
    batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
    useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
    false 表示不启用 beta 分组
    needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
    false 表示不启用分组确认
    grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次

    调度策略

    可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
    打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
    堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

    保护机制

    (正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

    对等和非对等

    您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
    非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
    对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。



    6 - 独立使用 Arklet

    Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处



    7 - 模块信息查看

    查看某个基座上所有安装的模块名称和状态

    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    -

    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    -

    查看某个基座上所有安装的模块详细信息

    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
    -

    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
    -

    替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

    8 - 模块Service

    ModuleService 简介

    K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 + + + +

    + +
    +
    +
    +
    +
    + + + + + +
    +
    +

    +这是本节的多页打印视图。 +点击此处打印. +

    +返回本页常规视图. +

    +
    + + + +

    模块运维

    + + + + + + + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    + +

    1 - 模块上线与下线

    + +

    模块上线

    +

    在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2
    +  operationStrategy:  # 此处可自定义发布运维策略
    +    upgradePolicy: installThenUninstall
    +    needConfirm: true
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy: # 此处可自定义调度策略
    +    schedulingPolicy: Scatter
    +

    ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

    +

    模块下线

    +

    在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

    +
    kubectl delete yourmoduledeployment --namespace yournamespace
    +

    其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    2 - 模块发布

    + +

    模块发布

    +

    修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
    +        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2
    +  operationStrategy:
    +    upgradePolicy: install_then_uninstall
    +    needConfirm: true
    +    grayTimeBetweenBatchSeconds: 0
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy:
    +    schedulingPolicy: scatter
    +

    如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

    +

    模块回滚

    +

    重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    3 - 基座和模块不兼容发布

    + +

    步骤 1

    +

    修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

    +

    步骤 2

    +

    修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

    +

    步骤 3

    +

    使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    4 - 模块扩缩容与替换

    + +

    模块扩缩容

    +

    修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
    +  operationStrategy:
    +    upgradePolicy: installThenUninstall
    +    needConfirm: true
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy: # 此处可自定义调度策略
    +    schedulingPolicy: Scatter  
    +

    如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

    +

    模块替换

    +

    在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

    +
    kubectl delete yourmodule --namespace yournamespace
    +

    其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    5 - 模块发布运维策略

    + +

    运维策略

    +

    为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    字段名字段解释取值范围取值解释
    batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
    useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
    false 表示不启用 beta 分组
    needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
    false 表示不启用分组确认
    grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次
    +

    调度策略

    +

    可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
    +打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
    +堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

    +

    保护机制

    +

    (正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

    +

    对等和非对等

    +

    您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
    +非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
    +对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    6 - 独立使用 Arklet

    + +

    Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处

    +
    +
    + +
    + + + + + + + + + + + +
    + +

    7 - 模块信息查看

    + +

    查看某个基座上所有安装的模块名称和状态

    +
    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    +

    +
    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    +

    查看某个基座上所有安装的模块详细信息

    +
    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
    +

    +
    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
    +

    替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

    + +
    + + + + + + + + + + + +
    + +

    8 - 模块Service

    + +

    ModuleService 简介

    +

    K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 模块也支持 Module 相关的 Service ,在模块发布时自动创建一个 service 来服务模块,将安装在一个或一组 Pod 的模块公开为网络服务。 -具体见:OperationStrategy.ServiceStrategy

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample-provider
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 1
    -  operationStrategy:
    -    needConfirm: false
    -    grayTimeBetweenBatchSeconds: 120
    -    useBeta: false
    -    batchCount: 1
    -    upgradePolicy: install_then_uninstall
    -    serviceStrategy:
    -      enableModuleService: true
    -      port: 8080
    -      targetPort: 8080
    -  schedulingStrategy:
    -    schedulingPolicy: scatter
    -

    字段解释

    OperationStrategy.ServiceStrategy 字段解释如下:

    字段解释取值范围
    EnableModuleService开启模块servicetrue or false
    Port公开的端口1 到 65535
    TargetPortpod上要访问的端口1 到 65535

    示例

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
    -

    自动创建的模块的 service

    apiVersion: v1
    -kind: Service
    -metadata:
    -  creationTimestamp: "2023-11-03T09:52:22Z"
    -  name: dynamic-provider-service
    -  namespace: default
    -  resourceVersion: "28170024"
    -  uid: 1f85e468-65e3-4181-b40e-48959a069df5
    -spec:
    -  clusterIP: 10.0.147.22
    -  clusterIPs:
    -  - 10.0.147.22
    -  externalTrafficPolicy: Cluster
    -  internalTrafficPolicy: Cluster
    -  ipFamilies:
    -  - IPv4
    -  ipFamilyPolicy: SingleStack
    -  ports:
    -  - name: http
    -    nodePort: 32232
    -    port: 8080
    -    protocol: TCP
    -    targetPort: 8080
    -  selector:
    -    module.serverless.alipay.com/dynamic-provider: "true"
    -  sessionAffinity: None
    -  type: NodePort
    -status:
    -  loadBalancer: {}
    -

    9 - 所有 K8S 资源定义及部署方式

    资源文件位置

    1. ModuleDeployment CRD 定义
    2. ModuleReplicaset CRD 定义
    3. ModuleTemplate CRD 定义
    4. Module CRD 定义
    5. Role 定义
    6. RBAC 定义
    7. ServiceAccount 定义
    8. ModuleController 部署定义

    部署方式

    使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。



    - - \ No newline at end of file +具体见:OperationStrategy.ServiceStrategy

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample-provider
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 1
    +  operationStrategy:
    +    needConfirm: false
    +    grayTimeBetweenBatchSeconds: 120
    +    useBeta: false
    +    batchCount: 1
    +    upgradePolicy: install_then_uninstall
    +    serviceStrategy:
    +      enableModuleService: true
    +      port: 8080
    +      targetPort: 8080
    +  schedulingStrategy:
    +    schedulingPolicy: scatter
    +

    字段解释

    +

    OperationStrategy.ServiceStrategy 字段解释如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    字段解释取值范围
    EnableModuleService开启模块servicetrue or false
    Port公开的端口1 到 65535
    TargetPortpod上要访问的端口1 到 65535
    +

    示例

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
    +

    自动创建的模块的 service

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  creationTimestamp: "2023-11-03T09:52:22Z"
    +  name: dynamic-provider-service
    +  namespace: default
    +  resourceVersion: "28170024"
    +  uid: 1f85e468-65e3-4181-b40e-48959a069df5
    +spec:
    +  clusterIP: 10.0.147.22
    +  clusterIPs:
    +  - 10.0.147.22
    +  externalTrafficPolicy: Cluster
    +  internalTrafficPolicy: Cluster
    +  ipFamilies:
    +  - IPv4
    +  ipFamilyPolicy: SingleStack
    +  ports:
    +  - name: http
    +    nodePort: 32232
    +    port: 8080
    +    protocol: TCP
    +    targetPort: 8080
    +  selector:
    +    module.serverless.alipay.com/dynamic-provider: "true"
    +  sessionAffinity: None
    +  type: NodePort
    +status:
    +  loadBalancer: {}
    +
    +
    + + + + + + + + + + + +
    + +

    9 - 所有 K8S 资源定义及部署方式

    + +

    资源文件位置

    +
      +
    1. ModuleDeployment CRD 定义
    2. +
    3. ModuleReplicaset CRD 定义
    4. +
    5. ModuleTemplate CRD 定义
    6. +
    7. Module CRD 定义
    8. +
    9. Role 定义
    10. +
    11. RBAC 定义
    12. +
    13. ServiceAccount 定义
    14. +
    15. ModuleController 部署定义
    16. +
    +

    部署方式

    +

    使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。

    +
    +
    +
    + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + diff --git a/docs/public/docs/tutorials/module-operation/arklet-standalone-usage/index.html b/docs/public/docs/tutorials/module-operation/arklet-standalone-usage/index.html index ed7d4c14a..652cefb30 100644 --- a/docs/public/docs/tutorials/module-operation/arklet-standalone-usage/index.html +++ b/docs/public/docs/tutorials/module-operation/arklet-standalone-usage/index.html @@ -1,12 +1,483 @@ -独立使用 Arklet | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +独立使用 Arklet | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    独立使用 Arklet

    Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处



    -

    最后修改 September 22, 2023: add docs (efcf6b56)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    独立使用 Arklet

    + + +

    Arklet 作为 SOFAServerless 模块发布运维的 Agent(定位类似 K8S 的 Kubelet),可以完全脱离 ModuleController 独立使用。它暴露了一组安装卸载模块的 HTTP 接口,从而可以让 SOFAServerless 对接到您自己的发布运维平台,接口文档详见此处

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 September 22, 2023: add docs (efcf6b56) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/crd-definition/index.html b/docs/public/docs/tutorials/module-operation/crd-definition/index.html index bca162015..8ea2b6a0b 100644 --- a/docs/public/docs/tutorials/module-operation/crd-definition/index.html +++ b/docs/public/docs/tutorials/module-operation/crd-definition/index.html @@ -1,12 +1,506 @@ -所有 K8S 资源定义及部署方式 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +所有 K8S 资源定义及部署方式 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    所有 K8S 资源定义及部署方式

    资源文件位置

    1. ModuleDeployment CRD 定义
    2. ModuleReplicaset CRD 定义
    3. ModuleTemplate CRD 定义
    4. Module CRD 定义
    5. Role 定义
    6. RBAC 定义
    7. ServiceAccount 定义
    8. ModuleController 部署定义

    部署方式

    使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。



    -

    最后修改 November 17, 2023: update docs (6e17a46e)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    所有 K8S 资源定义及部署方式

    + + +

    资源文件位置

    +
      +
    1. ModuleDeployment CRD 定义
    2. +
    3. ModuleReplicaset CRD 定义
    4. +
    5. ModuleTemplate CRD 定义
    6. +
    7. Module CRD 定义
    8. +
    9. Role 定义
    10. +
    11. RBAC 定义
    12. +
    13. ServiceAccount 定义
    14. +
    15. ModuleController 部署定义
    16. +
    +

    部署方式

    +

    使用 kubectl apply 命令,依次 apply 上述 8 个资源文件,即可完成 ModuleController 部署。

    +
    +
    + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 17, 2023: update docs (6e17a46e) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/incompatible-base-and-module-upgrade/index.html b/docs/public/docs/tutorials/module-operation/incompatible-base-and-module-upgrade/index.html index 2dcd8ebc9..bdacdda24 100644 --- a/docs/public/docs/tutorials/module-operation/incompatible-base-and-module-upgrade/index.html +++ b/docs/public/docs/tutorials/module-operation/incompatible-base-and-module-upgrade/index.html @@ -1,20 +1,505 @@ -基座和模块不兼容发布 | SOFAServerless + + + + + + + + + + + + + + + + + + +基座和模块不兼容发布 | SOFAServerless + + + + + + + + + + + + + + - - +步骤 3 使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。"/> + + + + + + + + + + + + + + + + + + + -

    基座和模块不兼容发布

    步骤 1

    修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

    步骤 2

    修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

    步骤 3

    使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。



    -

    最后修改 October 26, 2023: add styles (6b806506)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    基座和模块不兼容发布

    + + +

    步骤 1

    +

    修改基座代码和模块代码,然后将基座构建为新版本的镜像,将模块构建为新版本的代码包(Java 就是 Jar 包)。

    +

    步骤 2

    +

    修改模块对应的 ModuleDeployment.spec.template.spec.module.url 为新的模块代码包地址。

    +

    步骤 3

    +

    使用 K8S Deployment 发布基座到新版本镜像(会触发基座容器的替换或重启),基座容器启动时会拉取 ModuleDeployment 上最新的模块代码包地址,从而实现了基座与模块的不兼容变更(即同时发布)。

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 October 26, 2023: add styles (6b806506) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/index.html b/docs/public/docs/tutorials/module-operation/index.html index 6f75c56f7..e63ea2bfa 100644 --- a/docs/public/docs/tutorials/module-operation/index.html +++ b/docs/public/docs/tutorials/module-operation/index.html @@ -1,12 +1,558 @@ -模块运维 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +模块运维 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    模块运维

    -

    最后修改 November 14, 2023: add build and deploy (1950dff0)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块运维

    + + + +
    + + + + + + + + +
    + + +
    +
    + 模块上线与下线 +
    +

    +
    + + +
    +
    + 模块发布 +
    +

    +
    + + +
    +
    + 基座和模块不兼容发布 +
    +

    +
    + + +
    +
    + 模块扩缩容与替换 +
    +

    +
    + + +
    +
    + 模块发布运维策略 +
    +

    +
    + + +
    +
    + 独立使用 Arklet +
    +

    +
    + + +
    +
    + 模块信息查看 +
    +

    +
    + + +
    +
    + 模块Service +
    +

    +
    + + +
    +
    + 所有 K8S 资源定义及部署方式 +
    +

    +
    + + +
    + +
    + + + + + + +
    + +
    +
    + 最后修改 November 14, 2023: add build and deploy (1950dff0) +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/module-deployment-and-rollback/index.html b/docs/public/docs/tutorials/module-operation/module-deployment-and-rollback/index.html index 46faa7fb3..91aa8849c 100644 --- a/docs/public/docs/tutorials/module-operation/module-deployment-and-rollback/index.html +++ b/docs/public/docs/tutorials/module-operation/module-deployment-and-rollback/index.html @@ -1,49 +1,533 @@ -模块发布 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块发布 | SOFAServerless + + + + + + + + + + + + + + - - +apiVersion: serverless.alipay.com/v1alpha1 kind: ModuleDeployment metadata: labels: app.kubernetes.io/name: moduledeployment app.kubernetes.io/instance: moduledeployment-sample app.kubernetes.io/part-of: module-controller app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: module-controller name: moduledeployment-sample spec: baseDeploymentName: dynamic-stock-deployment template: spec: module: name: provider version: '2.0.0' # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布 # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改 url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar replicas: 2 operationStrategy: upgradePolicy: install_then_uninstall needConfirm: true grayTimeBetweenBatchSeconds: 0 useBeta: false batchCount: 2 schedulingStrategy: schedulingPolicy: scatter 如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略。"/> + + + + + + + + + + + + + + + + + + + -

    模块发布

    模块发布

    修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
    -        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2
    -  operationStrategy:
    -    upgradePolicy: install_then_uninstall
    -    needConfirm: true
    -    grayTimeBetweenBatchSeconds: 0
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy:
    -    schedulingPolicy: scatter
    -

    如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

    模块回滚

    重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。



    -

    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块发布

    + + +

    模块发布

    +

    修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现新版本模块的分组发布,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '2.0.0'  # 注意:这里将 version 字段从 1.0.2 修改为了 2.0.0 即可实现模块新版本分组发布
    +        # 注意:url 字段可以修改为新的 jar 包地址,也可以不用修改
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2
    +  operationStrategy:
    +    upgradePolicy: install_then_uninstall
    +    needConfirm: true
    +    grayTimeBetweenBatchSeconds: 0
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy:
    +    schedulingPolicy: scatter
    +

    如果要自定义模块发布运维策略可配置 operationStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现分组发布。

    +

    模块回滚

    +

    重新修改 ModuleDeployment.spec.template.spec.module.version 字段和 ModuleDeployment.spec.template.spec.module.url(可选)字段并重新 apply,即可实现模块的分组回滚发布。

    +
    +
    + + +
    + + + + + + +
    + + +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/module-information-viewing/index.html b/docs/public/docs/tutorials/module-operation/module-information-viewing/index.html index fd0f306f3..76c33d75f 100644 --- a/docs/public/docs/tutorials/module-operation/module-information-viewing/index.html +++ b/docs/public/docs/tutorials/module-operation/module-information-viewing/index.html @@ -1,24 +1,508 @@ -模块信息查看 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块信息查看 | SOFAServerless + - - +kubectl describe module -n &lt;namespace&gt; -l serverless.alipay.com/base-instance-name=&lt;pod-name&gt; 替换&lt;pod-ip&gt;为需要查看的基座ip,&lt;pod-name&gt;为需要查看的基座名称,&lt;namespace&gt;为需要查看资源的namespace"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    模块信息查看

    查看某个基座上所有安装的模块名称和状态

    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    -

    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    -

    查看某个基座上所有安装的模块详细信息

    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
    -

    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
    -

    替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

    -

    最后修改 November 6, 2023: 修改文档位置 (a1caba32)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块信息查看

    + + +

    查看某个基座上所有安装的模块名称和状态

    +
    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    +

    +
    kubectl get module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name> -o custom-columns=NAME:.metadata.name,STATUS:.status.status
    +

    查看某个基座上所有安装的模块详细信息

    +
    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-ip=<pod-ip>
    +

    +
    kubectl describe module -n <namespace> -l serverless.alipay.com/base-instance-name=<pod-name>
    +

    替换<pod-ip>为需要查看的基座ip,<pod-name>为需要查看的基座名称,<namespace>为需要查看资源的namespace

    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 6, 2023: 修改文档位置 (a1caba32) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/module-online-and-offline/index.html b/docs/public/docs/tutorials/module-operation/module-online-and-offline/index.html index 6a46d043b..40c3a586a 100644 --- a/docs/public/docs/tutorials/module-operation/module-online-and-offline/index.html +++ b/docs/public/docs/tutorials/module-operation/module-online-and-offline/index.html @@ -1,48 +1,533 @@ -模块上线与下线 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块上线与下线 | SOFAServerless + + + + + + + + + + + + + + - - +apiVersion: serverless.alipay.com/v1alpha1 kind: ModuleDeployment metadata: labels: app.kubernetes.io/name: moduledeployment app.kubernetes.io/instance: moduledeployment-sample app.kubernetes.io/part-of: module-controller app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: module-controller name: moduledeployment-sample spec: baseDeploymentName: dynamic-stock-deployment template: spec: module: name: provider version: '1.0.2' url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar replicas: 2 operationStrategy: # 此处可自定义发布运维策略 upgradePolicy: installThenUninstall needConfirm: true useBeta: false batchCount: 2 schedulingStrategy: # 此处可自定义调度策略 schedulingPolicy: Scatter ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释。"/> + + + + + + + + + + + + + + + + + + + -

    模块上线与下线

    模块上线

    在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2
    -  operationStrategy:  # 此处可自定义发布运维策略
    -    upgradePolicy: installThenUninstall
    -    needConfirm: true
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy: # 此处可自定义调度策略
    -    schedulingPolicy: Scatter
    -

    ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

    模块下线

    在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

    kubectl delete yourmoduledeployment --namespace yournamespace
    -

    其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。



    -

    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块上线与下线

    + + +

    模块上线

    +

    在 K8S 集群中创建一个 ModuleDeployment CR 资源即可完成模块上线,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2
    +  operationStrategy:  # 此处可自定义发布运维策略
    +    upgradePolicy: installThenUninstall
    +    needConfirm: true
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy: # 此处可自定义调度策略
    +    schedulingPolicy: Scatter
    +

    ModuleDeployment 所有字段可参考 ModuleDeployment CRD 字段解释
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 创建 ModuleDeployment CR 一样能实现模块分组上线。

    +

    模块下线

    +

    在 K8S 集群中删除一个 ModuleDeployment CR 资源即可完成模块下线,例如:

    +
    kubectl delete yourmoduledeployment --namespace yournamespace
    +

    其中 yourmoduledeployment 替换成您的 ModuleDeployment 名字(ModuleDeployment 的 metadata name),yournamespace 替换成您的 namespace。
    如果要自定义模块发布运维策略(比如分组、Beta、暂停等)可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 ModuleDeployment CR 一样能实现模块分组下线。

    +
    +
    + + +
    + + + + + + +
    + + +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/module-scale-and-replace/index.html b/docs/public/docs/tutorials/module-operation/module-scale-and-replace/index.html index dc20982bd..385d7d3a4 100644 --- a/docs/public/docs/tutorials/module-operation/module-scale-and-replace/index.html +++ b/docs/public/docs/tutorials/module-operation/module-scale-and-replace/index.html @@ -1,48 +1,533 @@ -模块扩缩容与替换 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块扩缩容与替换 | SOFAServerless + + + + + + + + + + + + + + - - +apiVersion: serverless.alipay.com/v1alpha1 kind: ModuleDeployment metadata: labels: app.kubernetes.io/name: moduledeployment app.kubernetes.io/instance: moduledeployment-sample app.kubernetes.io/part-of: module-controller app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: module-controller name: moduledeployment-sample spec: baseDeploymentName: dynamic-stock-deployment template: spec: module: name: provider version: '1.0.2' url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar replicas: 2 # 注意:在此处修改模块实例 Module 副本数,实现扩缩容 operationStrategy: upgradePolicy: installThenUninstall needConfirm: true useBeta: false batchCount: 2 schedulingStrategy: # 此处可自定义调度策略 schedulingPolicy: Scatter 如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略。"/> + + + + + + + + + + + + + + + + + + + -

    模块扩缩容与替换

    模块扩缩容

    修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    -

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
    -  operationStrategy:
    -    upgradePolicy: installThenUninstall
    -    needConfirm: true
    -    useBeta: false
    -    batchCount: 2
    -  schedulingStrategy: # 此处可自定义调度策略
    -    schedulingPolicy: Scatter  
    -

    如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

    模块替换

    在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

    kubectl delete yourmodule --namespace yournamespace
    -

    其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。



    -

    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块扩缩容与替换

    + + +

    模块扩缩容

    +

    修改 ModuleDeployment CR 的 replicas 字段并重新 apply,即可实现模块扩缩容,例如:

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment.yaml --namespace yournamespace
    +

    其中 deployment_v1alpha1_moduledeployment.yaml 替换成您的 ModuleDeployment 定义 yaml 文件,yournamespace 替换成您的 namespace。module-deployment_v1alpha1_moduledeployment.yaml 完整内容如下:

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 2  # 注意:在此处修改模块实例 Module 副本数,实现扩缩容
    +  operationStrategy:
    +    upgradePolicy: installThenUninstall
    +    needConfirm: true
    +    useBeta: false
    +    batchCount: 2
    +  schedulingStrategy: # 此处可自定义调度策略
    +    schedulingPolicy: Scatter  
    +

    如果要自定义模块发布运维策略可配置 operationStrategy 和 schedulingStrategy,具体可参考模块发布运维策略
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 修改 ModuleDeployment CR 一样能实现扩缩容。

    +

    模块替换

    +

    在 K8S 集群中删除一个 Module CR 资源即可完成模块替换,例如:

    +
    kubectl delete yourmodule --namespace yournamespace
    +

    其中 yourmodule 替换成您的 Module CR 实体名字(Module 的 metadata name),yournamespace 替换成您的 namespace。
    样例演示的是使用 kubectl 方式,直接调用 K8S APIServer 删除 Module CR 一样能实现模块替换。

    +
    +
    + + +
    + + + + + + +
    + + +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/module-service/index.html b/docs/public/docs/tutorials/module-operation/module-service/index.html index e50e165b5..517470def 100644 --- a/docs/public/docs/tutorials/module-operation/module-service/index.html +++ b/docs/public/docs/tutorials/module-operation/module-service/index.html @@ -1,79 +1,588 @@ -模块Service | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +模块Service | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    模块Service

    ModuleService 简介

    K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 + + + +

    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块Service

    + + +

    ModuleService 简介

    +

    K8S 通过 Service ,将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。 模块也支持 Module 相关的 Service ,在模块发布时自动创建一个 service 来服务模块,将安装在一个或一组 Pod 的模块公开为网络服务。 -具体见:OperationStrategy.ServiceStrategy

    apiVersion: serverless.alipay.com/v1alpha1
    -kind: ModuleDeployment
    -metadata:
    -  labels:
    -    app.kubernetes.io/name: moduledeployment
    -    app.kubernetes.io/instance: moduledeployment-sample
    -    app.kubernetes.io/part-of: module-controller
    -    app.kubernetes.io/managed-by: kustomize
    -    app.kubernetes.io/created-by: module-controller
    -  name: moduledeployment-sample-provider
    -spec:
    -  baseDeploymentName: dynamic-stock-deployment
    -  template:
    -    spec:
    -      module:
    -        name: provider
    -        version: '1.0.2'
    -        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    -  replicas: 1
    -  operationStrategy:
    -    needConfirm: false
    -    grayTimeBetweenBatchSeconds: 120
    -    useBeta: false
    -    batchCount: 1
    -    upgradePolicy: install_then_uninstall
    -    serviceStrategy:
    -      enableModuleService: true
    -      port: 8080
    -      targetPort: 8080
    -  schedulingStrategy:
    -    schedulingPolicy: scatter
    -

    字段解释

    OperationStrategy.ServiceStrategy 字段解释如下:

    字段解释取值范围
    EnableModuleService开启模块servicetrue or false
    Port公开的端口1 到 65535
    TargetPortpod上要访问的端口1 到 65535

    示例

    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
    -

    自动创建的模块的 service

    apiVersion: v1
    -kind: Service
    -metadata:
    -  creationTimestamp: "2023-11-03T09:52:22Z"
    -  name: dynamic-provider-service
    -  namespace: default
    -  resourceVersion: "28170024"
    -  uid: 1f85e468-65e3-4181-b40e-48959a069df5
    -spec:
    -  clusterIP: 10.0.147.22
    -  clusterIPs:
    -  - 10.0.147.22
    -  externalTrafficPolicy: Cluster
    -  internalTrafficPolicy: Cluster
    -  ipFamilies:
    -  - IPv4
    -  ipFamilyPolicy: SingleStack
    -  ports:
    -  - name: http
    -    nodePort: 32232
    -    port: 8080
    -    protocol: TCP
    -    targetPort: 8080
    -  selector:
    -    module.serverless.alipay.com/dynamic-provider: "true"
    -  sessionAffinity: None
    -  type: NodePort
    -status:
    -  loadBalancer: {}
    -
    -

    - - \ No newline at end of file +具体见:OperationStrategy.ServiceStrategy

    +
    apiVersion: serverless.alipay.com/v1alpha1
    +kind: ModuleDeployment
    +metadata:
    +  labels:
    +    app.kubernetes.io/name: moduledeployment
    +    app.kubernetes.io/instance: moduledeployment-sample
    +    app.kubernetes.io/part-of: module-controller
    +    app.kubernetes.io/managed-by: kustomize
    +    app.kubernetes.io/created-by: module-controller
    +  name: moduledeployment-sample-provider
    +spec:
    +  baseDeploymentName: dynamic-stock-deployment
    +  template:
    +    spec:
    +      module:
    +        name: provider
    +        version: '1.0.2'
    +        url: http://serverless-opensource.oss-cn-shanghai.aliyuncs.com/module-packages/stable/dynamic-provider-1.0.2-ark-biz.jar
    +  replicas: 1
    +  operationStrategy:
    +    needConfirm: false
    +    grayTimeBetweenBatchSeconds: 120
    +    useBeta: false
    +    batchCount: 1
    +    upgradePolicy: install_then_uninstall
    +    serviceStrategy:
    +      enableModuleService: true
    +      port: 8080
    +      targetPort: 8080
    +  schedulingStrategy:
    +    schedulingPolicy: scatter
    +

    字段解释

    +

    OperationStrategy.ServiceStrategy 字段解释如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    字段解释取值范围
    EnableModuleService开启模块servicetrue or false
    Port公开的端口1 到 65535
    TargetPortpod上要访问的端口1 到 65535
    +

    示例

    +
    kubectl apply -f sofa-serverless/module-controller/config/samples/module-deployment_v1alpha1_moduledeployment_provider.yaml --namespace yournamespace
    +

    自动创建的模块的 service

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  creationTimestamp: "2023-11-03T09:52:22Z"
    +  name: dynamic-provider-service
    +  namespace: default
    +  resourceVersion: "28170024"
    +  uid: 1f85e468-65e3-4181-b40e-48959a069df5
    +spec:
    +  clusterIP: 10.0.147.22
    +  clusterIPs:
    +  - 10.0.147.22
    +  externalTrafficPolicy: Cluster
    +  internalTrafficPolicy: Cluster
    +  ipFamilies:
    +  - IPv4
    +  ipFamilyPolicy: SingleStack
    +  ports:
    +  - name: http
    +    nodePort: 32232
    +    port: 8080
    +    protocol: TCP
    +    targetPort: 8080
    +  selector:
    +    module.serverless.alipay.com/dynamic-provider: "true"
    +  sessionAffinity: None
    +  type: NodePort
    +status:
    +  loadBalancer: {}
    +
    + +
    + + + + + + +
    + + +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/module-operation/operation-and-scheduling-strategy/index.html b/docs/public/docs/tutorials/module-operation/operation-and-scheduling-strategy/index.html index 5dcb19b2a..2734ef348 100644 --- a/docs/public/docs/tutorials/module-operation/operation-and-scheduling-strategy/index.html +++ b/docs/public/docs/tutorials/module-operation/operation-and-scheduling-strategy/index.html @@ -1,24 +1,552 @@ -模块发布运维策略 | SOFAServerless + + + + + + + + + + + + + + + + + + +模块发布运维策略 | SOFAServerless + + + + + + + + + + + + + + - - +false 表示不启用分组确认 grayTime 每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次 0 - N 批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次 调度策略 可以为基座 K8S Pod Deployment 配置 Label “serverless."/> + + + + + + + + + + + + + + + + + + + -

    模块发布运维策略

    运维策略

    为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

    字段名字段解释取值范围取值解释
    batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
    useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
    false 表示不启用 beta 分组
    needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
    false 表示不启用分组确认
    grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次

    调度策略

    可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
    打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
    堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

    保护机制

    (正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

    对等和非对等

    您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
    非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
    对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。



    -

    最后修改 October 30, 2023: update home (f751b32b)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    模块发布运维策略

    + + +

    运维策略

    +

    为了实现生产环境的无损变更,模块发布运维提供了安全可靠的变更能力,用户可以在 ModuleDeployment CR spec 的 operationStrategy 中,配置发布运维的变更策略。operationStrategy 内具体字段解释如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    字段名字段解释取值范围取值解释
    batchCount分批发布运维批次数1 - N分 1 - N 批次发布运维模块
    useBeta是否启用 beta 分组发布。启用 beta 分组发布会让第一批次只有一个 IP 做灰度,剩下的 IP 再划分成 (batchCount - 1) 批true 或 falsetrue 表示启用 beta 分组
    false 表示不启用 beta 分组
    needConfirm是否启用分组确认。启用后每一批次模块发布运维后,都会暂停,修改 ModuleDeployment.spec.pause 字段为 false 后,则运维继续true 或 falsetrue 表示启用分组确认
    false 表示不启用分组确认
    grayTime每一个发布运维批次完成后,sleep 多少时间才能继续执行下一个批次0 - N批次间的灰度时长,单位秒,0 表示批次完成后立即执行下一批次,N 表示批次完成后 sleep N 秒再执行下一批次
    +

    调度策略

    +

    可以为基座 K8S Pod Deployment 配置 Label “serverless.alipay.com/max-module-count”,指定每个 Pod 最多可以安装多少个模块。支持配置为 0 - N 的整数。模块支持打散调度和堆叠调度。
    +打散调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 scatter。打散调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最少的机器上去安装。
    +堆叠调度:设置 ModuleDeployment.spec.schedulingStrategy.schedulingPolicy 为 stacking。堆叠调度表示在模块上线、扩容、替换时,优先把模块调度到模块数最多且没达到基座 max-module-count 上限的机器上去安装。

    +

    保护机制

    +

    (正在开发中,10.15 上线) 您可以配置 ModuleDeployment.spec.maxUnavailable 指定模块在发布运维过程中,最多有几个模块副本可以同时处在不可用状态。模块发布运维需要更新 K8S Service 并卸载模块,会导致该模块副本不可用。配置为 50% 表示模块发布运维的一个批次,必须保证至少 50% 的模块副本可用,否则 ModuleDeployment.status 会展示报错信息。

    +

    对等和非对等

    +

    您可以配置 ModuleDeployment.spec.replicas 指定模块采用对等还是非对等部署架构。
    +非对等架构:设置 ModuleDeployment.spec.replicas 为 **0 - N **表示非对等架构。非对等架构下必须要为 ModuleDeployment、ModueRepicaSet 设置副本数,因此非对等架构下支持模块的扩容和缩容操作。
    +对等架构:设置 ModuleDeployment.spec.replicas 为 **-1 表示对等架构。**对等架构下,K8S Pod Deployment 有多少副本数模块就自动安装到多少个 Pod,模块的副本数始终与 K8S Pod Deployment 副本数一致。因此对等架构下不支持模块的扩缩容操作。对等架构正在建设中,预计 10.30 发布。

    +
    +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 October 30, 2023: update home (f751b32b) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/tutorials/trial_step_by_step/index.html b/docs/public/docs/tutorials/trial_step_by_step/index.html index caddd0c50..4551fb8b6 100644 --- a/docs/public/docs/tutorials/trial_step_by_step/index.html +++ b/docs/public/docs/tutorials/trial_step_by_step/index.html @@ -1,156 +1,732 @@ -基座与模块并行开发验证 | SOFAServerless + + + + + + + + + + + + + + + + + + +基座与模块并行开发验证 | SOFAServerless + + + + + + + + + + + + + + - - +在 **pom.xml **里增加必要的依赖 <properties> <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version> </properties> <dependencies> <dependency> <groupId>com.alipay.sofa.serverless</groupId> <artifactId>sofa-serverless-base-starter</artifactId> <version>${sofa.serverless.runtime.version}</version> </dependency> </dependencies> 理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里。 +<dependency> <groupId>com.alipay.sofa</groupId> <artifactId>web-ark-plugin</artifactId> </dependency> 点击编译器启动基座。 2. 模块 1 接入改造 添加模块需要的依赖和打包插件 <plugins> <!--这里添加ark 打包插件--> <plugin> <groupId>com.alipay.sofa</groupId> <artifactId>sofa-ark-maven-plugin</artifactId> <executions> <execution> <id>default-cli</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> <configuration> <skipArkExecutable>true</skipArkExecutable> <outputDirectory>./target</outputDirectory> <bizName>${替换为模块名}</bizName> <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath> <declaredMode>true</declaredMode> <!"/> + + + + + + + + + + + + + + + + + + + -

    基座与模块并行开发验证

    欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 -首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

    1. 合并部署
    2. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)

    本文实验工程代码在:开源仓库 samples 目录库里

    场景一:合并部署

    先介绍第一个场景多应用合并部署,整体流程如下: -image.png

    可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

    1. 基座接入改造

    1. 为 **application.properties **增加应用名(如果没有的话):

    spring.application.name=${基座应用名}

    1. 在 **pom.xml **里增加必要的依赖
    <properties>
    -    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    -</properties>
    -<dependencies>
    -    <dependency>
    -        <groupId>com.alipay.sofa.serverless</groupId>
    -        <artifactId>sofa-serverless-base-starter</artifactId>
    -        <version>${sofa.serverless.runtime.version}</version>
    -    </dependency>
    -</dependencies>
    -

    理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

        <dependency>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>web-ark-plugin</artifactId>
    -    </dependency>
    -
    1. 点击编译器启动基座。

    2. 模块 1 接入改造

    1. 添加模块需要的依赖和打包插件
    <plugins>
    -    <!--这里添加ark 打包插件-->
    -    <plugin>
    -        <groupId>com.alipay.sofa</groupId>
    -        <artifactId>sofa-ark-maven-plugin</artifactId>
    -        <executions>
    -            <execution>
    -                <id>default-cli</id>
    -                <goals>
    -                    <goal>repackage</goal>
    -                </goals>
    -            </execution>
    -        </executions>
    -        <configuration>
    -            <skipArkExecutable>true</skipArkExecutable>
    -            <outputDirectory>./target</outputDirectory>
    -            <bizName>${替换为模块名}</bizName>
    -            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
    -            <declaredMode>true</declaredMode>
    -            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
    -            <packExcludesConfig>rules.txt</packExcludesConfig>
    -        </configuration>
    -    </plugin>
    -    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    -    <plugin>
    -        <!--原来 spring-boot 打包插件 -->
    -        <groupId>org.springframework.boot</groupId>
    -        <artifactId>spring-boot-maven-plugin</artifactId>
    -    </plugin>
    -</plugins>
    -
    1. 参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

    2. 开发模块,例如增加 Rest Controller,提供 Rest 接口

    @RestController
    -public class SampleController {
    -    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    -
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -        String appName = applicationContext.getApplicationName();
    -        LOGGER.info("{} web test: into sample controller", appName);
    -        return String.format("hello to %s deploy", appName);
    -    }
    -}
    -
    1. 点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

    2. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

    hello to ${模块1名} deploy
    -

    3. 模块 1 开发与验证

    开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

    1. 修改 Rest 代码
    @RestController
    -public class SampleController {
    -    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    -
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @RequestMapping(value = "/", method = RequestMethod.GET)
    -    public String hello() {
    -        String appName = applicationContext.getApplicationName();
    -        LOGGER.info("{} web test v2: into sample controller", appName);
    -        return String.format("hello to %s deploy v2", appName);
    -    }
    -}
    -
    1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
    hello to ${模块1名} deploy v2
    -

    4. 模块 2 接入改造、开发与验证

    模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

    场景二:中台应用

    中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: -image.png

    可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI。 -接下来我们将介绍与合并部署不同的(即新增的)操作细节。

    1. 基座完成通信类和 SPI 的定义

    在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 -通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

    1. 新建 bundle,定义接口类
    public class ProductInfo {
    -    private String  name;
    -    private String  author;
    -    private String  src;
    -    private Integer orderCount;
    -}
    -
    1. 定义 SPI
    public interface StrategyService {
    -    List<ProductInfo> strategy(List<ProductInfo> products);
    -    String getAppName();
    -}
    -

    2. 模块 1 引入通信类基座并实现基座 SPI

    在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

    1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
    2. 定义 SPI 实现
    @Service
    -public class StrategyServiceImpl implements StrategyService {
    -
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @Override
    -    public List<ProductInfo> strategy(List<ProductInfo> products) {
    -        return products;
    -    }
    -
    -    @Override
    -    public String getAppName() {
    -        return applicationContext.getApplicationName();
    -    }
    -}
    -
    1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回

    biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

    默认的 products 列表
    -

    3. 模块 2 引入通信类基座并实现基座 SPI

    与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

    @Service
    -public class StrategyServiceImpl implements StrategyService {
    -    @Autowired
    -    private ApplicationContext applicationContext;
    -
    -    @Override
    -    public List<ProductInfo> strategy(List<ProductInfo> products) {
    -        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
    -        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
    -        return products;
    -    }
    -
    -    @Override
    -    public String getAppName() {
    -        return applicationContext.getApplicationName();
    -    }
    -}
    -
    更改排序后的 products 列表
    -

    基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

    文档中的链接地址

    1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
    2. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
    3. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
    4. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
    5. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/
    -

    最后修改 November 24, 2023: add order number (b8d62aa1)
    - - \ No newline at end of file + + + +
    + +
    +
    +
    +
    + + +
    + + + + + +
    +

    基座与模块并行开发验证

    + + +

    欢迎使用 SOFAServerless 完成多 SpringBoot 应用合并部署与动态更新模块!本文将详细介绍操作流程与方法,希望能够帮助大家节省资源、提高研发效率。 +首先,利用 SOFAServerless 完成合并部署与动态更新模块,适用于两种典型场景:

    +
      +
    1. 合并部署
    2. +
    3. 中台应用(该场景需要先完成合并部署,再完成中台应用 demo)
    4. +
    +
    +

    本文实验工程代码在:开源仓库 samples 目录库里

    +
    +

    场景一:合并部署

    +

    先介绍第一个场景多应用合并部署,整体流程如下: +image.png

    +

    可以看到,整体上需要完成的动作是基座/模块接入改造后进行开发与验证,而基座与模块的合并部署动作都是可以并行的。接下来我们将逐步介绍操作细节。

    +

    1. 基座接入改造

    +
      +
    1. 为 **application.properties **增加应用名(如果没有的话):
    2. +
    +

    spring.application.name=${基座应用名}

    +
      +
    1. 在 **pom.xml **里增加必要的依赖
    2. +
    +
    <properties>
    +    <sofa.serverless.runtime.version>0.5.3</sofa.serverless.runtime.version>
    +</properties>
    +<dependencies>
    +    <dependency>
    +        <groupId>com.alipay.sofa.serverless</groupId>
    +        <artifactId>sofa-serverless-base-starter</artifactId>
    +        <version>${sofa.serverless.runtime.version}</version>
    +    </dependency>
    +</dependencies>
    +

    +

    理论上增加这个依赖就可以了,但由于本 demo 需要演示多个 web 模块应用使用一个端口合并部署,需要再引入 web-ark-plugin 依赖,详细原理查看这里

    +
        <dependency>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>web-ark-plugin</artifactId>
    +    </dependency>
    +
      +
    1. 点击编译器启动基座。
    2. +
    +

    2. 模块 1 接入改造

    +
      +
    1. 添加模块需要的依赖和打包插件
    2. +
    +
    <plugins>
    +    <!--这里添加ark 打包插件-->
    +    <plugin>
    +        <groupId>com.alipay.sofa</groupId>
    +        <artifactId>sofa-ark-maven-plugin</artifactId>
    +        <executions>
    +            <execution>
    +                <id>default-cli</id>
    +                <goals>
    +                    <goal>repackage</goal>
    +                </goals>
    +            </execution>
    +        </executions>
    +        <configuration>
    +            <skipArkExecutable>true</skipArkExecutable>
    +            <outputDirectory>./target</outputDirectory>
    +            <bizName>${替换为模块名}</bizName>
    +            <webContextPath>${模块自定义的 web context path,需要与其他模块不同}</webContextPath>
    +            <declaredMode>true</declaredMode>
    +            <!--  配置模块自动排包列表,从 github 下载 rules.txt,并放在模块根目录的 conf/ark/ 目录下,下载地址:https://github.com/sofastack/sofa-serverless/blob/master/samples/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt  -->
    +            <packExcludesConfig>rules.txt</packExcludesConfig>
    +        </configuration>
    +    </plugin>
    +    <!--  构建出普通 SpringBoot fatjar,支持独立部署时使用,如果不需要可以删除  -->
    +    <plugin>
    +        <!--原来 spring-boot 打包插件 -->
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-maven-plugin</artifactId>
    +    </plugin>
    +</plugins>
    +
      +
    1. +

      参考官网模块瘦身里自动排包部分,下载排包配置文件 rules.txt,放在在 conf/ark/ 目录下

      +
    2. +
    3. +

      开发模块,例如增加 Rest Controller,提供 Rest 接口

      +
    4. +
    +
    @RestController
    +public class SampleController {
    +    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    +
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +        String appName = applicationContext.getApplicationName();
    +        LOGGER.info("{} web test: into sample controller", appName);
    +        return String.format("hello to %s deploy", appName);
    +    }
    +}
    +
      +
    1. +

      点击这里下载 Arkctl,mac/linux 电脑放入 **/usr/local/bin** 目录中,windows 可以考虑直接放在项目根目录下

      +
    2. +
    3. +

      执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回。显示正常,进入下一步。

      +
    4. +
    +
    hello to ${模块1名} deploy
    +

    3. 模块 1 开发与验证

    +

    开发与验证需要完成修改代码并发布 V2 版本。具体操作如下:

    +
      +
    1. 修改 Rest 代码
    2. +
    +
    @RestController
    +public class SampleController {
    +    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    +
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @RequestMapping(value = "/", method = RequestMethod.GET)
    +    public String hello() {
    +        String appName = applicationContext.getApplicationName();
    +        LOGGER.info("{} web test v2: into sample controller", appName);
    +        return String.format("hello to %s deploy v2", appName);
    +    }
    +}
    +
      +
    1. 执行 arkctl deploy 构建部署,成功后 curl localhost:8080/${模块1 web context path}/ 验证服务返回
    2. +
    +
    hello to ${模块1名} deploy v2
    +

    4. 模块 2 接入改造、开发与验证

    +

    模块 2 同样采用上述步骤2⃣️3⃣️,即模块 1 接入改造与验证的操作流程。

    +

    场景二:中台应用

    +

    中台应用的特点是基座有复杂的编排逻辑去定义对外暴露服务和业务所需的 SPI。模块应用来实现这些 SPI 接口,往往会对一个接口在多个模块里定义多个不同的实现。整体流程如下: +image.png

    +

    可以看到,与场景一合并部署操作不同的是,需要在基座接入改造与开发验证中间新增一步通信类和 SPI 的定义模块接入改造与开发验证中间新增一步引入通信类基座并实现基座 SPI。 +接下来我们将介绍与合并部署不同的(即新增的)操作细节。

    +

    1. 基座完成通信类和 SPI 的定义

    +

    在合并部署接入改造的基础上,需要完成通信类和 SPI 的定义。 +通信类需要以 **独立 bundle **的方式存在,才能被模块引入。可参考以下方式:

    +
      +
    1. 新建 bundle,定义接口类
    2. +
    +
    public class ProductInfo {
    +    private String  name;
    +    private String  author;
    +    private String  src;
    +    private Integer orderCount;
    +}
    +
      +
    1. 定义 SPI
    2. +
    +
    public interface StrategyService {
    +    List<ProductInfo> strategy(List<ProductInfo> products);
    +    String getAppName();
    +}
    +

    2. 模块 1 引入通信类基座并实现基座 SPI

    +

    在上文合并部署模块 1 接入改造 demo 的基础上,引入通信类,然后定义 SPI 实现。

    +
      +
    1. 引入通信类和对应 SPI 定义,只需要在 pom 里引入基座定义的通信 bundle
    2. +
    3. 定义 SPI 实现
    4. +
    +
    @Service
    +public class StrategyServiceImpl implements StrategyService {
    +
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @Override
    +    public List<ProductInfo> strategy(List<ProductInfo> products) {
    +        return products;
    +    }
    +
    +    @Override
    +    public String getAppName() {
    +        return applicationContext.getApplicationName();
    +    }
    +}
    +
      +
    1. 执行 arkctl deploy 构建部署,成功后用 curl localhost:8080/${基座服务入口}/biz1/ 验证服务返回
    2. +
    +

    biz1 传入是为了使基座根据不同的参数找到不同的 SPI 实现,执行不同的逻辑。传入的方式可以有很多种,这里用最简单方式——从 **path **里传入。

    +
    默认的 products 列表
    +

    3. 模块 2 引入通信类基座并实现基座 SPI

    +

    与模块 1 操作相同,需要注意执行 arkctl deploy 构建部署时,成功后 curl localhost:8080/${基座服务入口}/biz2/ 验证服务返回。同理,**biz2 **传入是为了基座根据不同的参数,找到不同的 SPI 实现,执行不同逻辑。

    +
    @Service
    +public class StrategyServiceImpl implements StrategyService {
    +    @Autowired
    +    private ApplicationContext applicationContext;
    +
    +    @Override
    +    public List<ProductInfo> strategy(List<ProductInfo> products) {
    +        Collections.sort(products, (m, n) -> n.getOrderCount() - m.getOrderCount());
    +        products.stream().forEach(p -> p.setName(p.getName()+"("+p.getOrderCount()+")"));
    +        return products;
    +    }
    +
    +    @Override
    +    public String getAppName() {
    +        return applicationContext.getApplicationName();
    +    }
    +}
    +
    更改排序后的 products 列表
    +

    基于上述操作,就可以继续进行上文中模块 开发与验证 的操作了。整体流程丝滑易上手,欢迎试用!

    +

    文档中的链接地址

    +
      +
    1. 本实验工程样例地址:https://github.com/sofastack/sofa-serverless/tree/master/samples/springboot-samples/web/tomcat
    2. +
    3. web-ark-plugin 原理: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-multi-web-component-deploy/
    4. +
    5. 自动排包原理与配置文件下载:https://sofaserverless.gitee.io/docs/tutorials/module-development/module-slimming/#%E4%B8%80%E9%94%AE%E8%87%AA%E5%8A%A8%E7%98%A6%E8%BA%AB
    6. +
    7. Arkctl 下载地址:https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0
    8. +
    9. 本文档地址:https://sofaserverless.gitee.io/docs/tutorials/trial_step_by_step/
    10. +
    + + +
    + + + + + + +
    + + +
    +
    + 最后修改 November 24, 2023: add order number (b8d62aa1) +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + +
    +
    + + + + + + + +
    + +
    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/docs/public/docs/video-training/_print/index.html b/docs/public/docs/video-training/_print/index.html index dd773235f..8fc1ca8d6 100644 --- a/docs/public/docs/video-training/_print/index.html +++ b/docs/public/docs/video-training/_print/index.html @@ -1,9 +1,242 @@ -视频教程 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +视频教程 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    这是本节的多页打印视图。 -点击此处打印.

    返回本页常规视图.

    视频教程

      SOFAServerless 模块本地开发与上线视频教程

      小贴士: 仅需两分钟时间



      该视频的详细文字版教程请点击此处查看。

      SOFAServerless 平台和研发框架完整视频教程

      步骤 1:点击此处注册开源学堂账号。

      步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      +
      + + + + + +
      +
      +

      +这是本节的多页打印视图。 +点击此处打印. +

      +返回本页常规视图. +

      +
      + + + +

      视频教程

      + + + + + +
        + + + + + + + + +
      + + +
      +

      SOFAServerless 模块本地开发与上线视频教程

      +

      小贴士: 仅需两分钟时间

      + +
      +
      +

      该视频的详细文字版教程请点击此处查看。

      +

      SOFAServerless 平台和研发框架完整视频教程

      +

      步骤 1:点击此处注册开源学堂账号。

      +

      步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

      + +
      +
      + + + + + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + diff --git a/docs/public/docs/video-training/index.html b/docs/public/docs/video-training/index.html index abfcb88e0..dd25f4bec 100644 --- a/docs/public/docs/video-training/index.html +++ b/docs/public/docs/video-training/index.html @@ -1,13 +1,504 @@ -视频教程 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +视频教程 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      视频教程

      SOFAServerless 模块本地开发与上线视频教程

      小贴士: 仅需两分钟时间



      该视频的详细文字版教程请点击此处查看。

      SOFAServerless 平台和研发框架完整视频教程

      步骤 1:点击此处注册开源学堂账号。

      步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

      -

      最后修改 November 24, 2023: update docs (f841325d)
      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      + + +
      + + + + + +
      +

      视频教程

      + + +

      SOFAServerless 模块本地开发与上线视频教程

      +

      小贴士: 仅需两分钟时间

      + +
      +
      +

      该视频的详细文字版教程请点击此处查看。

      +

      SOFAServerless 平台和研发框架完整视频教程

      +

      步骤 1:点击此处注册开源学堂账号。

      +

      步骤 2:在开源学堂首页点击上方 “学习” 选项卡,然后点击进入 “SOFAServerless 研发框架与产品介绍”,点击 “开始学习”

      + +
      + + + + + + + + + +
      + +
      + + + + + + +
      + +
      +
      + 最后修改 November 24, 2023: update docs (f841325d) +
      +
      + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/home/index.html b/docs/public/home/index.html index c0d2fcb4c..684a28bc9 100644 --- a/docs/public/home/index.html +++ b/docs/public/home/index.html @@ -1,11 +1,773 @@ -SOFAServerless | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +SOFAServerless | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      让普通应用低成本享受 Serverless 体验,帮助企业降本增效!

      - -

      产品介绍

      SOFAServerless 是一种模块化 Serverless 技术解决方案,它能让普通应用低成本演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护, -与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。

      十亿级可统计的企业线上每分钟流量。50%企业需求交付效率提升。75%长尾应用机器数量减少。

      适用场景

      大幅加速应用构建和发布:传统应用镜像化构建+发布速度很慢,通过模块化方式,应用单次构建+发布耗时可从 5 分钟级减少到 1 分钟。

      实现 SDK 无感升级:借助 SOFAServerless 将应用依赖尽可能下沉到基座 (类似业务 Sidecar),可以实现 SDK 的无打扰升级。

      极致裁剪长尾应用资源成本:通过 SOFAServerless 将多个应用合并部署在一起,可以实现大量的长尾应用服务器裁撤。

      大幅提升应用研发协作效率:通过 SOFAServerless 将应用快速划分成多个模块 (代码包),且多个模块间可以同时迭代互不影响,进而大幅提升研发效率。

      简化中台业务资产沉淀:支持低成本将业务公共代码下沉到基座并在基座上长出各种轻薄的功能模块,从而让组织分工更加合理、需求交付更加高效。

      降低微服务的演进成本:支持业务架构低成本地在单体应用、多模块、独立微服务应用之间来回切换,从而轻松让应用架构与业务发展保持及时同步。

      大幅加速应用构建与发布传统应用镜像化构建 + 发布速度很慢,通过模块化方式,应用单次构建+发布耗时可从 5 分钟级减少到 1 分钟SDK 无感升级借助 SOFAServerless 将应用依赖尽可能下沉到基座 (类似业务 Sidecar),可以实现 SDK 的无打扰升级极致裁剪长尾应用资源成本通过 SOFAServerless 将多个应用合并部署在一起,可以实现大量的长尾应用服务器裁撤大幅提升应用研发协作效率通过 SOFAServerless 将应用快速划分成多个模块 (代码包),且多个模块间可以同时迭代互不影响,进而大幅提升研发效率简化中台业务资产沉淀支持低成本将业务公共代码下沉到基座并在基座上长出各种轻薄的功能模块,从而让组织分工更加合理、需求交付更加高效降低微服务演进成本支持业务架构低成本地在单体应用、多模块、独立微服务应用之间来回切换,从而轻松让应用架构与业务发展保持及时同步

      SOFAServerless 优势

      Speed as you need: 十秒级构建与启动,应用多个功能之间独立并行迭代无阻塞。

      Pay as you need: 模块粒度小,占用资源少,调度密度与资源复用率高。模块和基座支持自动弹性伸缩,按需部署。

      Deploy as you need: 灵活部署:模块可合并部署也可独立部署。变更影响面小:一次部署只涉及模块自身代码变更和对应的机器变更。

      Evolution as you need: 提供配套工具,传统应用能一键改造成模块,大应用能低成本拆分成模块,模块能轻松演进成微服务或者回到单体应用。

      Speed as you needPay as you needDeploy as you need灵活部署:模块可合并部署也可独立部署变更影响面小:一次部署只涉及模块自身代码变更和对应的机器变更模块粒度小,占用资源少,调度密度与资源复用率高。模块和基座支持自动弹性伸缩,按需部署十秒级构建与启动,应用多个功能之间独立并行迭代无阻塞Evolution as you need提供配套工具,传统应用能一键改造成模块,大应用能低成本拆分成模块,模块能轻松演进成微服务或者回到单体应用

      欢迎参与开源社区

      所有人都可以提交 Pull Request。 -欢迎参与 SOFAServerless 开源社区!

      欢迎加入社区协作钉钉群

      社区钉钉群号:24970018417

      欢迎加入社区协作微信群

      - - \ No newline at end of file + + + +
      + +
      +
      +
      + +
      +
      +
      +
      +
      + + +
      + +
      +
      + + + + + + + + + + + + + + + + + + + + + +
      +

      让普通应用低成本享受 Serverless 体验,帮助企业降本增效!

      + + + + + + + + + + +

      产品介绍

      +

      SOFAServerless 是一种模块化 Serverless 技术解决方案,它能让普通应用低成本演进为 Serverless 研发模式,让代码与资源解耦,轻松独立维护, +与此同时支持秒级构建部署、合并部署、动态伸缩等能力为用户提供极致的研发运维体验,最终帮助企业实现降本增效。

      +

      十亿级可统计的企业线上每分钟流量。50%企业需求交付效率提升。75%长尾应用机器数量减少。

      +
      + +
      +

      适用场景

      +

      大幅加速应用构建和发布:传统应用镜像化构建+发布速度很慢,通过模块化方式,应用单次构建+发布耗时可从 5 分钟级减少到 1 分钟。

      +

      实现 SDK 无感升级:借助 SOFAServerless 将应用依赖尽可能下沉到基座 (类似业务 Sidecar),可以实现 SDK 的无打扰升级。

      +

      极致裁剪长尾应用资源成本:通过 SOFAServerless 将多个应用合并部署在一起,可以实现大量的长尾应用服务器裁撤。

      +

      大幅提升应用研发协作效率:通过 SOFAServerless 将应用快速划分成多个模块 (代码包),且多个模块间可以同时迭代互不影响,进而大幅提升研发效率。

      +

      简化平台/中台搭建和业务资产沉淀:支持低成本将业务公共代码下沉到基座并在基座上长出各种轻薄的功能模块,从而让组织分工更加合理、需求交付更加高效。

      +

      降低微服务的演进成本:支持业务架构低成本地在单体应用、多模块、独立微服务应用之间来回切换,从而轻松让应用架构与业务发展保持及时同步。

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 大幅加速应用构建与发布 + + + 传统应用镜像化构建 + 发布速度很慢, + 通过模块化方式,应用单次构建+发布 + 耗时可从 5 分钟级减少到 1 分钟 + + + + + + + + + + + + + + + + + + SDK 无感升级 + + + 借助 SOFAServerless 将应用依赖尽可 + 能下沉到基座 (类似业务 Sidecar),可 + 以实现 SDK 的无打扰升级 + + + + + + + + + + + + + + + + + + + + + + + + 极致裁剪长尾应用资源成本 + + + 通过 SOFAServerless 将多个应用合并 + 部署在一起,可以实现大量的长尾应用 + 服务器裁撤 + + + + + + + + + + + + + + + + + + + 大幅提升应用研发协作效率 + + + 通过 SOFAServerless 将应用快速划分成 + 多个模块 (代码包),且多个模块间可以同 + 时迭代互不影响,进而大幅提升研发效率 + + + + + + + + + + + + + + + + + 简化平台/中台搭建和业务资产沉淀 + + + 支持低成本将业务公共代码下沉到基座并 + 在基座上长出各种轻薄的功能模块,从而 + 让组织分工更加合理、需求交付更加高效 + + + + + + + + + + + + + + + + + + 降低微服务演进成本 + + + 支持业务架构低成本地在单体应用、多模 + 块、独立微服务应用之间来回切换,从而 + 轻松让应用架构与业务发展保持及时同步 + + + + + +
      +

      SOFAServerless 优势

      +

      Speed as you need: 十秒级构建与启动,应用多个功能之间独立并行迭代无阻塞。

      +

      Pay as you need: 模块粒度小,占用资源少,调度密度与资源复用率高。模块和基座支持自动弹性伸缩,按需部署。

      +

      Deploy as you need: 灵活部署:模块可合并部署也可独立部署。变更影响面小:一次部署只涉及模块自身代码变更和对应的机器变更。

      +

      Evolution as you need: 提供配套工具,传统应用能一键改造成模块,大应用能低成本拆分成模块,模块能轻松演进成微服务或者回到单体应用。

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Speed as you need + + + + + + + + + Pay as you need + + + + + + + Deploy as you need + + + 灵活部署:模块可合并部署也可独立部署 + 变更影响面小:一次部署只涉及模块自身 + 代码变更和对应的机器变更 + + + 模块粒度小,占用资源少,调度密度与 + 资源复用率高。模块和基座支持自动弹 + 性伸缩,按需部署 + + + 十秒级构建与启动,应用多个功能之间 + 独立并行迭代无阻塞 + + + + + + + + Evolution as you need + + + 提供配套工具,传统应用能一键改造成模 + 块,大应用能低成本拆分成模块,模块能 + 轻松演进成微服务或者回到单体应用 + + + + + + +
      +
      +
      +
      +

      欢迎参与开源社区

      +

      所有人都可以提交 Pull Request。 +欢迎参与 SOFAServerless 开源社区!

      +
      +
      +

      欢迎加入社区协作钉钉群

      +

      + +

      +

      社区钉钉群号:24970018417

      +
      +
      +

      欢迎加入社区协作微信群

      +

      + +

      +
      +
      +
      +
      + + +
      +
      +
      +
      + +
      + + +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/index.html b/docs/public/index.html index 6d01922d5..3f06a40e2 100644 --- a/docs/public/index.html +++ b/docs/public/index.html @@ -1,7 +1,182 @@ -SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
      - - \ No newline at end of file + + + +
      + +
      +
      +
      + + + + + + +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/index.xml b/docs/public/index.xml index dff46a070..1c42ff3b3 100644 --- a/docs/public/index.xml +++ b/docs/public/index.xml @@ -1,47 +1,119 @@ -SOFAServerless –/Recent content on SOFAServerlessHugo -- gohugo.iozh-cnSat, 21 Oct 2023 10:28:35 +0800Blog: 干货文章与视频/blog/2023/09/22/%E5%B9%B2%E8%B4%A7%E6%96%87%E7%AB%A0%E4%B8%8E%E8%A7%86%E9%A2%91/Fri, 22 Sep 2023 10:28:35 +0800/blog/2023/09/22/%E5%B9%B2%E8%B4%A7%E6%96%87%E7%AB%A0%E4%B8%8E%E8%A7%86%E9%A2%91/ -<h2 id="技术会议">技术会议</h2> -<h3 id="qcon-大会---sofaserverless-微服务新架构的探索与实践">QCon 大会 - SOFAServerless 微服务新架构的探索与实践</h3> -<p>举办时间:2023.09<br /> -会议 PPT:<a href="https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/outer-materials/%E8%9A%82%E8%9A%81%20SOFAServerless%20%E6%9E%81%E8%87%B4%E9%99%8D%E6%9C%AC%E5%A2%9E%E6%95%88%E6%96%B9%E6%A1%88%20-%20%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%96%B0%E6%9E%B6%E6%9E%84%E7%9A%84%E6%8E%A2%E7%B4%A2%E4%B8%8E%E5%AE%9E%E8%B7%B5.pdf">点击此处</a></p> -<h3 id="sofastack-开源四周年大会---蚂蚁-sofaserverless-技术体系化介绍">SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍</h3> -<p>举办时间:2022.07<br /> -会议视频:<a href="https://www.bilibili.com/video/BV1nU4y1B7u3/?spm_id_from=333.999.0.0">https://www.bilibili.com/video/BV1nU4y1B7u3</a><br /> -会议 PPT:<a href="https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/outer-materials/%E8%9A%82%E8%9A%81%20SOFAServerless%20%E6%8A%80%E6%9C%AF%E4%BD%93%E7%B3%BB%E5%8C%96%E4%BB%8B%E7%BB%8D.pptx">点击此处</a></p> -<h2 id="技术分享视频">技术分享视频</h2> -<h3 id="开源人---sofaark-类隔离框架底层原理">开源人 - SOFAArk 类隔离框架底层原理</h3> -<p>发布时间:2022.06<br /> -<a href="https://www.bilibili.com/video/BV1gS4y1i7Fg/?spm_id_from=333.999.0.0">https://www.bilibili.com/video/BV1gS4y1i7Fg</a></p> -<h2 id="技术分享文章">技术分享文章</h2> -<h3 id="蚂蚁-sofaserverless-微服务新架构的探索与实践">蚂蚁 SOFAServerless 微服务新架构的探索与实践</h3> -<p>发布时间:2023.08<br /> -<a href="https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ">https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ</a></p> -<h3 id="sofaserverless-应用架构助力业务-10-倍效率提升探索微服务隔离与共享的新平衡">SOFAServerless 应用架构助力业务 10 倍效率提升,探索微服务隔离与共享的新平衡</h3> -<p>发布时间:2023.07<br /> -<a href="https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg">https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg</a></p> -<br/>Blog: 获奖情况/blog/2023/09/22/%E8%8E%B7%E5%A5%96%E6%83%85%E5%86%B5/Fri, 22 Sep 2023 10:28:34 +0800/blog/2023/09/22/%E8%8E%B7%E5%A5%96%E6%83%85%E5%86%B5/ -<h2 id="中国信通院云原生技术创新案例奖">中国信通院云原生技术创新案例奖</h2> -<p><img src="https://intranetproxy.alipay.com/skylark/lark/0/2023/png/671/1693822410136-3b6fcf6e-77e4-4ac6-a9c9-5f38956f99a5.png#clientId=u9064f970-d3c7-4&amp;from=paste&amp;height=587&amp;id=ud233717f&amp;originHeight=786&amp;originWidth=1048&amp;originalType=binary&amp;ratio=2&amp;rotation=0&amp;showTitle=false&amp;size=1318357&amp;status=done&amp;style=none&amp;taskId=u50372608-5aa1-4502-98f4-4438f3f21d3&amp;title=&amp;width=782" alt="image.png"><br /><img src="https://intranetproxy.alipay.com/skylark/lark/0/2023/png/671/1693822416217-2145fd25-7f3b-4462-b4f3-cfb89a78e7dc.png#clientId=u9064f970-d3c7-4&amp;from=paste&amp;height=619&amp;id=ua764cfd6&amp;originHeight=786&amp;originWidth=590&amp;originalType=binary&amp;ratio=2&amp;rotation=0&amp;showTitle=false&amp;size=769897&amp;status=done&amp;style=none&amp;taskId=u053ec7ec-6536-422a-98ca-467348bc07d&amp;title=&amp;width=465" alt="image.png"></p> -<br/>Blog: 社区会议纪要/blog/2023/09/22/%E7%A4%BE%E5%8C%BA%E4%BC%9A%E8%AE%AE%E7%BA%AA%E8%A6%81/Fri, 22 Sep 2023 10:28:33 +0800/blog/2023/09/22/%E7%A4%BE%E5%8C%BA%E4%BC%9A%E8%AE%AE%E7%BA%AA%E8%A6%81/ -<h3 id="sofaserverless-230919-社区会议">SOFAServerless 23.09.19 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/100">https://github.com/sofastack/sofa-serverless/issues/100</a> <br/></p> -<h3 id="sofaserverless-230904-社区会议">SOFAServerless 23.09.04 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/44">https://github.com/sofastack/sofa-serverless/issues/44</a> <br/></p> -<h3 id="sofaserverless-230821-社区会议">SOFAServerless 23.08.21 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/38">https://github.com/sofastack/sofa-serverless/issues/38</a><br/> -视频回放:<a href="https://www.bilibili.com/video/BV19r4y1R761/">https://www.bilibili.com/video/BV19r4y1R761</a><br/></p> -<h3 id="sofaserverless-230807-社区会议">SOFAServerless 23.08.07 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/13">https://github.com/sofastack/sofa-serverless/issues/13</a><br/></p> -<h3 id="sofaserverless-230703-社区会议">SOFAServerless 23.07.03 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/1">https://github.com/sofastack/sofa-serverless/issues/1</a><br/> -视频回放:<a href="https://www.bilibili.com/video/BV1dh4y1f7KW/">https://www.bilibili.com/video/BV1dh4y1f7KW</a><br/></p> -<h3 id="sofaserverless-230605-社区会议">SOFAServerless 23.06.05 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-ark/issues/661">https://github.com/sofastack/sofa-ark/issues/661</a><br/></p> -<h3 id="sofaserverless-230508-社区会议">SOFAServerless 23.05.08 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-ark/issues/636">https://github.com/sofastack/sofa-ark/issues/636</a><br/> -视频回放:<a href="https://www.bilibili.com/video/BV1Qs4y1D7Lv/">https://www.bilibili.com/video/BV1Qs4y1D7Lv</a><br/></p> -<h3 id="sofaserverless-230403-社区会议">SOFAServerless 23.04.03 社区会议</h3> -<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-ark/issues/635">https://github.com/sofastack/sofa-ark/issues/635</a><br/> -视频回放:<a href="https://www.bilibili.com/video/BV1f84y1K7qd/">https://www.bilibili.com/video/BV1f84y1K7qd</a><br/></p> -<br/>Blog: 产品发布记录/blog/2023/09/22/%E4%BA%A7%E5%93%81%E5%8F%91%E5%B8%83%E8%AE%B0%E5%BD%95/Fri, 22 Sep 2023 10:28:32 +0800/blog/2023/09/22/%E4%BA%A7%E5%93%81%E5%8F%91%E5%B8%83%E8%AE%B0%E5%BD%95/ -<p>请阅读 GitHub 上的<a href="https://github.com/sofastack/sofa-serverless/releases/">产品发布记录</a>。</p> -<br/> \ No newline at end of file + + + SOFAServerless – + / + Recent content on SOFAServerless + Hugo -- gohugo.io + zh-cn + Sat, 21 Oct 2023 10:28:35 +0800 + + + + + + + + + + Blog: 干货文章与视频 + /blog/2023/09/22/%E5%B9%B2%E8%B4%A7%E6%96%87%E7%AB%A0%E4%B8%8E%E8%A7%86%E9%A2%91/ + Fri, 22 Sep 2023 10:28:35 +0800 + + /blog/2023/09/22/%E5%B9%B2%E8%B4%A7%E6%96%87%E7%AB%A0%E4%B8%8E%E8%A7%86%E9%A2%91/ + + + + <h2 id="技术会议">技术会议</h2> +<h3 id="kcd-开发者交流会-深圳站">KCD 开发者交流会 深圳站</h3> +<p>举办时间:20231216 <br/> +会议 PPT: <a href="https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/outer-materials/SOFAServerless%20%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%96%B0%E6%9E%B6%E6%9E%84%E7%9A%84%E6%8E%A2%E7%B4%A2%E4%B8%8E%E5%AE%9E%E8%B7%B5_20231217_v0.9.1.pdf">点击此处下载</a></p> +<h3 id="qcon-大会---sofaserverless-微服务新架构的探索与实践">QCon 大会 - SOFAServerless 微服务新架构的探索与实践</h3> +<p>举办时间:2023.09<br /> +会议 PPT:<a href="https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/outer-materials/%E8%9A%82%E8%9A%81%20SOFAServerless%20%E6%9E%81%E8%87%B4%E9%99%8D%E6%9C%AC%E5%A2%9E%E6%95%88%E6%96%B9%E6%A1%88%20-%20%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%96%B0%E6%9E%B6%E6%9E%84%E7%9A%84%E6%8E%A2%E7%B4%A2%E4%B8%8E%E5%AE%9E%E8%B7%B5.pdf">点击此处</a></p> +<h3 id="sofastack-开源四周年大会---蚂蚁-sofaserverless-技术体系化介绍">SOFAStack 开源四周年大会 - 蚂蚁 SOFAServerless 技术体系化介绍</h3> +<p>举办时间:2022.07<br /> +会议视频:<a href="https://www.bilibili.com/video/BV1nU4y1B7u3/?spm_id_from=333.999.0.0">https://www.bilibili.com/video/BV1nU4y1B7u3</a><br /> +会议 PPT:<a href="https://serverless-opensource.oss-cn-shanghai.aliyuncs.com/outer-materials/%E8%9A%82%E8%9A%81%20SOFAServerless%20%E6%8A%80%E6%9C%AF%E4%BD%93%E7%B3%BB%E5%8C%96%E4%BB%8B%E7%BB%8D.pptx">点击此处</a></p> +<h2 id="技术分享视频">技术分享视频</h2> +<h3 id="开源人---sofaark-类隔离框架底层原理">开源人 - SOFAArk 类隔离框架底层原理</h3> +<p>发布时间:2022.06<br /> +<a href="https://www.bilibili.com/video/BV1gS4y1i7Fg/?spm_id_from=333.999.0.0">https://www.bilibili.com/video/BV1gS4y1i7Fg</a></p> +<h2 id="技术分享文章">技术分享文章</h2> +<h3 id="蚂蚁-sofaserverless-微服务新架构的探索与实践">蚂蚁 SOFAServerless 微服务新架构的探索与实践</h3> +<p>发布时间:2023.08<br /> +<a href="https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ">https://mp.weixin.qq.com/s/dSyWTEascUkF4Jd3_RBjVQ</a></p> +<h3 id="sofaserverless-应用架构助力业务-10-倍效率提升探索微服务隔离与共享的新平衡">SOFAServerless 应用架构助力业务 10 倍效率提升,探索微服务隔离与共享的新平衡</h3> +<p>发布时间:2023.07<br /> +<a href="https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg">https://mp.weixin.qq.com/s/JQYtk-Z54udV5Fhaf_akjg</a></p> +<br/> + + + + + + Blog: 获奖情况 + /blog/2023/09/22/%E8%8E%B7%E5%A5%96%E6%83%85%E5%86%B5/ + Fri, 22 Sep 2023 10:28:34 +0800 + + /blog/2023/09/22/%E8%8E%B7%E5%A5%96%E6%83%85%E5%86%B5/ + + + + <h2 id="中国信通院云原生技术创新案例奖">中国信通院云原生技术创新案例奖</h2> +<p><img src="https://intranetproxy.alipay.com/skylark/lark/0/2023/png/671/1693822410136-3b6fcf6e-77e4-4ac6-a9c9-5f38956f99a5.png#clientId=u9064f970-d3c7-4&amp;from=paste&amp;height=587&amp;id=ud233717f&amp;originHeight=786&amp;originWidth=1048&amp;originalType=binary&amp;ratio=2&amp;rotation=0&amp;showTitle=false&amp;size=1318357&amp;status=done&amp;style=none&amp;taskId=u50372608-5aa1-4502-98f4-4438f3f21d3&amp;title=&amp;width=782" alt="image.png"><br /><img src="https://intranetproxy.alipay.com/skylark/lark/0/2023/png/671/1693822416217-2145fd25-7f3b-4462-b4f3-cfb89a78e7dc.png#clientId=u9064f970-d3c7-4&amp;from=paste&amp;height=619&amp;id=ua764cfd6&amp;originHeight=786&amp;originWidth=590&amp;originalType=binary&amp;ratio=2&amp;rotation=0&amp;showTitle=false&amp;size=769897&amp;status=done&amp;style=none&amp;taskId=u053ec7ec-6536-422a-98ca-467348bc07d&amp;title=&amp;width=465" alt="image.png"></p> +<br/> + + + + + + Blog: 社区会议纪要 + /blog/2023/09/22/%E7%A4%BE%E5%8C%BA%E4%BC%9A%E8%AE%AE%E7%BA%AA%E8%A6%81/ + Fri, 22 Sep 2023 10:28:33 +0800 + + /blog/2023/09/22/%E7%A4%BE%E5%8C%BA%E4%BC%9A%E8%AE%AE%E7%BA%AA%E8%A6%81/ + + + + <h3 id="sofaserverless-230919-社区会议">SOFAServerless 23.09.19 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/100">https://github.com/sofastack/sofa-serverless/issues/100</a> <br/></p> +<h3 id="sofaserverless-230904-社区会议">SOFAServerless 23.09.04 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/44">https://github.com/sofastack/sofa-serverless/issues/44</a> <br/></p> +<h3 id="sofaserverless-230821-社区会议">SOFAServerless 23.08.21 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/38">https://github.com/sofastack/sofa-serverless/issues/38</a><br/> +视频回放:<a href="https://www.bilibili.com/video/BV19r4y1R761/">https://www.bilibili.com/video/BV19r4y1R761</a><br/></p> +<h3 id="sofaserverless-230807-社区会议">SOFAServerless 23.08.07 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/13">https://github.com/sofastack/sofa-serverless/issues/13</a><br/></p> +<h3 id="sofaserverless-230703-社区会议">SOFAServerless 23.07.03 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-serverless/issues/1">https://github.com/sofastack/sofa-serverless/issues/1</a><br/> +视频回放:<a href="https://www.bilibili.com/video/BV1dh4y1f7KW/">https://www.bilibili.com/video/BV1dh4y1f7KW</a><br/></p> +<h3 id="sofaserverless-230605-社区会议">SOFAServerless 23.06.05 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-ark/issues/661">https://github.com/sofastack/sofa-ark/issues/661</a><br/></p> +<h3 id="sofaserverless-230508-社区会议">SOFAServerless 23.05.08 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-ark/issues/636">https://github.com/sofastack/sofa-ark/issues/636</a><br/> +视频回放:<a href="https://www.bilibili.com/video/BV1Qs4y1D7Lv/">https://www.bilibili.com/video/BV1Qs4y1D7Lv</a><br/></p> +<h3 id="sofaserverless-230403-社区会议">SOFAServerless 23.04.03 社区会议</h3> +<p>会议纪要详见:<a href="https://github.com/sofastack/sofa-ark/issues/635">https://github.com/sofastack/sofa-ark/issues/635</a><br/> +视频回放:<a href="https://www.bilibili.com/video/BV1f84y1K7qd/">https://www.bilibili.com/video/BV1f84y1K7qd</a><br/></p> +<br/> + + + + + + Blog: 产品发布记录 + /blog/2023/09/22/%E4%BA%A7%E5%93%81%E5%8F%91%E5%B8%83%E8%AE%B0%E5%BD%95/ + Fri, 22 Sep 2023 10:28:32 +0800 + + /blog/2023/09/22/%E4%BA%A7%E5%93%81%E5%8F%91%E5%B8%83%E8%AE%B0%E5%BD%95/ + + + + <p>请阅读 GitHub 上的<a href="https://github.com/sofastack/sofa-serverless/releases/">产品发布记录</a>。</p> +<br/> + + + + + + diff --git a/docs/public/js/main.min.1eb4262674b2d02aa8d18559fef13b166dbdfa627fd0a495c66e11577c026aa3.js b/docs/public/js/main.min.1eb4262674b2d02aa8d18559fef13b166dbdfa627fd0a495c66e11577c026aa3.js new file mode 100644 index 000000000..9a154cc11 --- /dev/null +++ b/docs/public/js/main.min.1eb4262674b2d02aa8d18559fef13b166dbdfa627fd0a495c66e11577c026aa3.js @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.2.3 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */(function(e,t){typeof exports=="object"&&typeof module!="undefined"?module.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis!="undefined"?globalThis:e||self,e.bootstrap=t())})(this,function(){"use strict";const ro=1e6,Jr=1e3,ut="transitionend",Xr=e=>e==null?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),Gr=e=>{do e+=Math.floor(Math.random()*ro);while(document.getElementById(e))return e},ss=e=>{let t=e.getAttribute("data-bs-target");if(!t||t==="#"){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&n!=="#"?n.trim():null}return t},ns=e=>{const t=ss(e);return t?document.querySelector(t)?t:null:null},v=e=>{const t=ss(e);return t?document.querySelector(t):null},Yr=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),o=Number.parseFloat(n);return!s&&!o?0:(t=t.split(",")[0],n=n.split(",")[0],(Number.parseFloat(t)+Number.parseFloat(n))*Jr)},es=e=>{e.dispatchEvent(new Event(ut))},g=e=>!!e&&typeof e=="object"&&(typeof e.jquery!="undefined"&&(e=e[0]),typeof e.nodeType!="undefined"),E=e=>g(e)?e.jquery?e[0]:e:typeof e=="string"&&e.length>0?document.querySelector(e):null,H=e=>{if(!g(e)||e.getClientRects().length===0)return!1;const n=getComputedStyle(e).getPropertyValue("visibility")==="visible",t=e.closest("details:not([open])");if(!t)return n;if(t!==e){const n=e.closest("summary");if(n&&n.parentNode!==t)return!1;if(n===null)return!1}return n},w=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(typeof e.disabled!="undefined"?e.disabled:e.hasAttribute("disabled")&&e.getAttribute("disabled")!=="false"),Jn=e=>{if(!document.documentElement.attachShadow)return null;if(typeof e.getRootNode=="function"){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?Jn(e.parentNode):null},Oe=()=>{},ne=e=>{e.offsetHeight},Xn=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,He=[],Pr=e=>{document.readyState==="loading"?(He.length||document.addEventListener("DOMContentLoaded",()=>{for(const e of He)e()}),He.push(e)):e()},a=()=>document.documentElement.dir==="rtl",c=e=>{Pr(()=>{const t=Xn();if(t){const n=e.NAME,s=t.fn[n];t.fn[n]=e.jQueryInterface,t.fn[n].Constructor=e,t.fn[n].noConflict=()=>(t.fn[n]=s,e.jQueryInterface)}})},y=e=>{typeof e=="function"&&e()},Yn=(e,t,n=!0)=>{if(!n){y(e);return}const i=5,a=Yr(t)+i;let s=!1;const o=({target:n})=>{if(n!==t)return;s=!0,t.removeEventListener(ut,o),y(e)};t.addEventListener(ut,o),setTimeout(()=>{s||es(t)},a)},Le=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return o===-1?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])},Nr=/[^.]*(?=\..*)\.|.*/,zr=/\..*/,Tr=/::\d+$/,Ge={};let Un=1;const Vn={mouseenter:"mouseover",mouseleave:"mouseout"},Ar=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function In(e,t){return t&&`${t}::${Un++}`||e.uidEvent||Un++}function Fn(e){const t=In(e);return e.uidEvent=t,Ge[t]=Ge[t]||{},Ge[t]}function xr(t,n){return function s(o){return st(o,{delegateTarget:t}),s.oneOff&&e.off(t,o.type,n),n.apply(t,[o])}}function Or(t,n,s){return function o(i){const a=t.querySelectorAll(n);for(let{target:r}=i;r&&r!==this;r=r.parentNode)for(const c of a){if(c!==r)continue;return st(i,{delegateTarget:r}),o.oneOff&&e.off(t,i.type,n,s),s.apply(r,[i])}}}function Sn(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function En(e,t,n){const o=typeof t=="string",i=o?n:t||n;let s=On(e);return Ar.has(s)||(s=e),[o,i,s]}function Cn(e,t,n,s,o){if(typeof t!="string"||!e)return;let[r,i,c]=En(t,n,s);if(t in Vn){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};i=e(i)}const d=Fn(e),u=d[c]||(d[c]={}),l=Sn(u,i,r?n:null);if(l){l.oneOff=l.oneOff&&o;return}const h=In(i,t.replace(Nr,"")),a=r?Or(e,n,i):xr(e,i);a.delegationSelector=r?n:null,a.callable=i,a.oneOff=o,a.uidEvent=h,u[h]=a,e.addEventListener(c,a,r)}function Me(e,t,n,s,o){const i=Sn(t[n],s,o);if(!i)return;e.removeEventListener(n,i,Boolean(o)),delete t[n][i.uidEvent]}function wr(e,t,n,s){const o=t[n]||{};for(const i of Object.keys(o))if(i.includes(s)){const s=o[i];Me(e,t,n,s.callable,s.delegationSelector)}}function On(e){return e=e.replace(zr,""),Vn[e]||e}const e={on(e,t,n,s){Cn(e,t,n,s,!1)},one(e,t,n,s){Cn(e,t,n,s,!0)},off(e,t,n,s){if(typeof t!="string"||!e)return;const[c,r,i]=En(t,n,s),l=i!==t,o=Fn(e),a=o[i]||{},d=t.startsWith(".");if(typeof r!="undefined"){if(!Object.keys(a).length)return;Me(e,o,i,r,c?n:null);return}if(d)for(const n of Object.keys(o))wr(e,o,n,t.slice(1));for(const n of Object.keys(a)){const s=n.replace(Tr,"");if(!l||t.includes(s)){const t=a[n];Me(e,o,i,t.callable,t.delegationSelector)}}},trigger(e,t,n){if(typeof t!="string"||!e)return null;const i=Xn(),l=On(t),d=t!==l;let s=null,a=!0,r=!0,c=!1;d&&i&&(s=i.Event(t,n),i(e).trigger(s),a=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),c=s.isDefaultPrevented());let o=new Event(t,{bubbles:a,cancelable:!0});return o=st(o,n),c&&o.preventDefault(),r&&e.dispatchEvent(o),o.defaultPrevented&&s&&s.preventDefault(),o}};function st(e,t){for(const[n,s]of Object.entries(t||{}))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get(){return s}})}return e}const x=new Map,it={set(e,t,n){x.has(e)||x.set(e,new Map);const s=x.get(e);if(!s.has(t)&&s.size!==0){console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`);return}s.set(t,n)},get(e,t){return x.has(e)?x.get(e).get(t)||null:null},remove(e,t){if(!x.has(e))return;const n=x.get(e);n.delete(t),n.size===0&&x.delete(e)}};function _n(e){if(e==="true")return!0;if(e==="false")return!1;if(e===Number(e).toString())return Number(e);if(e===""||e==="null")return null;if(typeof e!="string")return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function ht(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const b={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${ht(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${ht(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const o of n){let s=o.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),t[s]=_n(e.dataset[o])}return t},getDataAttribute(e,t){return _n(e.getAttribute(`data-bs-${ht(t)}`))}};class te{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=g(t)?b.getDataAttribute(t,"config"):{};return{...this.constructor.Default,...typeof n=="object"?n:{},...g(t)?b.getDataAttributes(t):{},...typeof e=="object"?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const n of Object.keys(t)){const s=t[n],o=e[n],i=g(o)?"element":Xr(o);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const _r="5.2.3";class u extends te{constructor(e,t){if(super(),e=E(e),!e)return;this._element=e,this._config=this._getConfig(t),it.set(this._element,this.constructor.DATA_KEY,this)}dispose(){it.remove(this._element,this.constructor.DATA_KEY),e.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){Yn(e,t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return it.get(E(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,typeof t=="object"?t:null)}static get VERSION(){return _r}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const Se=(t,n="hide")=>{const o=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;e.on(document,o,`[data-bs-dismiss="${s}"]`,function(e){if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),w(this))return;const o=v(this)||this.closest(`.${s}`),i=t.getOrCreateInstance(o);i[n]()})},jr="alert",dr="bs.alert",vn=`.${dr}`,rr=`close${vn}`,Ja=`closed${vn}`,Qa="fade",Ga="show";class de extends u{static get NAME(){return jr}close(){const t=e.trigger(this._element,rr);if(t.defaultPrevented)return;this._element.classList.remove(Ga);const n=this._element.classList.contains(Qa);this._queueCallback(()=>this._destroyElement(),this._element,n)}_destroyElement(){this._element.remove(),e.trigger(this._element,Ja),this.dispose()}static jQueryInterface(e){return this.each(function(){const t=de.getOrCreateInstance(this);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)})}}Se(de,"close"),c(de);const Ka="button",$a="bs.button",Ia=`.${$a}`,Da=".data-api",ba="active",fn='[data-bs-toggle="button"]',va=`click${Ia}${Da}`;class fe extends u{static get NAME(){return Ka}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle(ba))}static jQueryInterface(e){return this.each(function(){const t=fe.getOrCreateInstance(this);e==="toggle"&&t[e]()})}}e.on(document,va,fn,e=>{e.preventDefault();const t=e.target.closest(fn),n=fe.getOrCreateInstance(t);n.toggle()}),c(fe);const t={find(e,t=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(t,e))},findOne(e,t=document.documentElement){return Element.prototype.querySelector.call(t,e)},children(e,t){return[].concat(...e.children).filter(e=>e.matches(t))},parents(e,t){const s=[];let n=e.parentNode.closest(t);for(;n;)s.push(n),n=n.parentNode.closest(t);return s},prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!w(e)&&H(e))}},ga="swipe",$=".bs.swipe",pa=`touchstart${$}`,ma=`touchmove${$}`,ua=`touchend${$}`,na=`pointerdown${$}`,Xi=`pointerup${$}`,$i="touch",Vi="pen",Pi="pointer-event",Li=40,Ni={endCallback:null,leftCallback:null,rightCallback:null},Di={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Re extends te{constructor(e,t){if(super(),this._element=e,!e||!Re.isSupported())return;this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents()}static get Default(){return Ni}static get DefaultType(){return Di}static get NAME(){return ga}dispose(){e.off(this._element,$)}_start(e){if(!this._supportPointerEvents){this._deltaX=e.touches[0].clientX;return}this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),y(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){const e=Math.abs(this._deltaX);if(e<=Li)return;const t=e/this._deltaX;if(this._deltaX=0,!t)return;y(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(e.on(this._element,na,e=>this._start(e)),e.on(this._element,Xi,e=>this._end(e)),this._element.classList.add(Pi)):(e.on(this._element,pa,e=>this._start(e)),e.on(this._element,ma,e=>this._move(e)),e.on(this._element,ua,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&(e.pointerType===Vi||e.pointerType===$i)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const is="carousel",Mi="bs.carousel",k=`.${Mi}`,Wt=".data-api",ki="ArrowLeft",yi="ArrowRight",ji=500,Z="next",K="prev",P="left",Ae="right",vi=`slide${k}`,Ve=`slid${k}`,di=`keydown${k}`,li=`mouseenter${k}`,oi=`mouseleave${k}`,ti=`dragstart${k}`,Zo=`load${k}${Wt}`,qo=`click${k}${Wt}`,Lt="carousel",ye="active",Wo="slide",Bo="carousel-item-end",Io="carousel-item-start",Po="carousel-item-next",Ro="carousel-item-prev",Mt=".active",Ct=".carousel-item",Lo=Mt+Ct,No=".carousel-item img",Do=".carousel-indicators",zo="[data-bs-slide], [data-bs-slide-to]",To='[data-bs-ride="carousel"]',bo={[ki]:Ae,[yi]:P},go={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},lo={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ie extends u{constructor(e,n){super(e,n),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=t.findOne(Do,this._element),this._addEventListeners(),this._config.ride===Lt&&this.cycle()}static get Default(){return go}static get DefaultType(){return lo}static get NAME(){return is}next(){this._slide(Z)}nextWhenVisible(){!document.hidden&&H(this._element)&&this.next()}prev(){this._slide(K)}pause(){this._isSliding&&es(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){if(!this._config.ride)return;if(this._isSliding){e.one(this._element,Ve,()=>this.cycle());return}this.cycle()}to(t){const n=this._getItems();if(t>n.length-1||t<0)return;if(this._isSliding){e.one(this._element,Ve,()=>this.to(t));return}const s=this._getItemIndex(this._getActive());if(s===t)return;const o=t>s?Z:K;this._slide(o,n[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&e.on(this._element,di,e=>this._keydown(e)),this._config.pause==="hover"&&(e.on(this._element,li,()=>this.pause()),e.on(this._element,oi,()=>this._maybeEnableCycle())),this._config.touch&&Re.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const n of t.find(No,this._element))e.on(n,ti,e=>e.preventDefault());const n=()=>{if(this._config.pause!=="hover")return;this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),ji+this._config.interval)},s={leftCallback:()=>this._slide(this._directionToOrder(P)),rightCallback:()=>this._slide(this._directionToOrder(Ae)),endCallback:n};this._swipeHelper=new Re(this._element,s)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=bo[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const s=t.findOne(Mt,this._indicatorsElement);s.classList.remove(ye),s.removeAttribute("aria-current");const n=t.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add(ye),n.setAttribute("aria-current","true"))}_updateInterval(){const e=this._activeElement||this._getActive();if(!e)return;const t=Number.parseInt(e.getAttribute("data-bs-interval"),10);this._config.interval=t||this._config.defaultInterval}_slide(t,n=null){if(this._isSliding)return;const o=this._getActive(),a=t===Z,s=n||Le(this._getItems(),o,a,this._config.wrap);if(s===o)return;const c=this._getItemIndex(s),l=n=>e.trigger(this._element,n,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(o),to:c}),d=l(vi);if(d.defaultPrevented)return;if(!o||!s)return;const u=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(c),this._activeElement=s;const i=a?Io:Bo,r=a?Po:Ro;s.classList.add(r),ne(s),o.classList.add(i),s.classList.add(i);const h=()=>{s.classList.remove(i,r),s.classList.add(ye),o.classList.remove(ye,r,i),this._isSliding=!1,l(Ve)};this._queueCallback(h,o,this._isAnimated()),u&&this.cycle()}_isAnimated(){return this._element.classList.contains(Wo)}_getActive(){return t.findOne(Lo,this._element)}_getItems(){return t.find(Ct,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return a()?e===P?K:Z:e===P?Z:K}_orderToDirection(e){return a()?e===K?P:Ae:e===K?Ae:P}static jQueryInterface(e){return this.each(function(){const t=ie.getOrCreateInstance(this,e);if(typeof e=="number"){t.to(e);return}if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}}e.on(document,qo,zo,function(e){const n=v(this);if(!n||!n.classList.contains(Lt))return;e.preventDefault();const t=ie.getOrCreateInstance(n),s=this.getAttribute("data-bs-slide-to");if(s){t.to(s),t._maybeEnableCycle();return}if(b.getDataAttribute(this,"slide")==="next"){t.next(),t._maybeEnableCycle();return}t.prev(),t._maybeEnableCycle()}),e.on(window,Zo,()=>{const e=t.find(To);for(const t of e)ie.getOrCreateInstance(t)}),c(ie);const os="collapse",ao="bs.collapse",ee=`.${ao}`,eo=".data-api",Zs=`show${ee}`,Xs=`shown${ee}`,Gs=`hide${ee}`,qs=`hidden${ee}`,Ks=`click${ee}${eo}`,rt="show",V="collapse",je="collapsing",Us="collapsed",Ws=`:scope .${V} .${V}`,$s="collapse-horizontal",Vs="width",Bs="height",Is=".collapse.show, .collapse.collapsing",et='[data-bs-toggle="collapse"]',Hs={parent:null,toggle:!0},Ps={parent:"(null|element)",toggle:"boolean"};class le extends u{constructor(e,n){super(e,n),this._isTransitioning=!1,this._triggerArray=[];const s=t.find(et);for(const e of s){const n=ns(e),o=t.find(n).filter(e=>e===this._element);n!==null&&o.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Hs}static get DefaultType(){return Ps}static get NAME(){return os}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let n=[];if(this._config.parent&&(n=this._getFirstLevelChildren(Is).filter(e=>e!==this._element).map(e=>le.getOrCreateInstance(e,{toggle:!1}))),n.length&&n[0]._isTransitioning)return;const s=e.trigger(this._element,Zs);if(s.defaultPrevented)return;for(const e of n)e.hide();const t=this._getDimension();this._element.classList.remove(V),this._element.classList.add(je),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const o=()=>{this._isTransitioning=!1,this._element.classList.remove(je),this._element.classList.add(V,rt),this._element.style[t]="",e.trigger(this._element,Xs)},i=t[0].toUpperCase()+t.slice(1),a=`scroll${i}`;this._queueCallback(o,this._element,!0),this._element.style[t]=`${this._element[a]}px`}hide(){if(this._isTransitioning||!this._isShown())return;const n=e.trigger(this._element,Gs);if(n.defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,ne(this._element),this._element.classList.add(je),this._element.classList.remove(V,rt);for(const e of this._triggerArray){const t=v(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0;const s=()=>{this._isTransitioning=!1,this._element.classList.remove(je),this._element.classList.add(V),e.trigger(this._element,qs)};this._element.style[t]="",this._queueCallback(s,this._element,!0)}_isShown(e=this._element){return e.classList.contains(rt)}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=E(e.parent),e}_getDimension(){return this._element.classList.contains($s)?Vs:Bs}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(et);for(const t of e){const n=v(t);n&&this._addAriaAndCollapsedClass([t],this._isShown(n))}}_getFirstLevelChildren(e){const n=t.find(Ws,this._config.parent);return t.find(e,this._config.parent).filter(e=>!n.includes(e))}_addAriaAndCollapsedClass(e,t){if(!e.length)return;for(const n of e)n.classList.toggle(Us,!t),n.setAttribute("aria-expanded",t)}static jQueryInterface(e){const t={};return typeof e=="string"&&/show|hide/.test(e)&&(t.toggle=!1),this.each(function(){const n=le.getOrCreateInstance(this,t);if(typeof e=="string"){if(typeof n[e]=="undefined")throw new TypeError(`No method named "${e}"`);n[e]()}})}}e.on(document,Ks,et,function(e){(e.target.tagName==="A"||e.delegateTarget&&e.delegateTarget.tagName==="A")&&e.preventDefault();const n=ns(this),s=t.find(n);for(const e of s)le.getOrCreateInstance(e,{toggle:!1}).toggle()}),c(le);var O,z,s="top",se,Ln,Bn,ae,Gn,Qn,Je,St,At,kt,Et,he,o="bottom",i="right",n="left",xe="auto",G=[s,o,i,n],F="start",W="end",$t="clippingParents",ze="viewport",L="popper",Kt="reference",Ie=G.reduce(function(e,t){return e.concat([t+"-"+F,t+"-"+W])},[]),Be=[].concat(G,[xe]).reduce(function(e,t){return e.concat([t,t+"-"+F,t+"-"+W])},[]),Gt="beforeRead",Xt="read",Qt="afterRead",Zt="beforeMain",Jt="main",en="afterMain",tn="beforeWrite",nn="write",sn="afterWrite",on=[Gt,Xt,Qt,Zt,Jt,en,tn,nn,sn];function f(e){return e?(e.nodeName||"").toLowerCase():null}function r(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t?t.defaultView||window:window}return e}function D(e){var t=r(e).Element;return e instanceof t||e instanceof Element}function l(e){var t=r(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}function Ye(e){if(typeof ShadowRoot=="undefined")return!1;var t=r(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}function Ss(e){var t=e.state;Object.keys(t.elements).forEach(function(e){var o=t.styles[e]||{},s=t.attributes[e]||{},n=t.elements[e];if(!l(n)||!f(n))return;Object.assign(n.style,o),Object.keys(s).forEach(function(e){var t=s[e];t===!1?n.removeAttribute(e):n.setAttribute(e,t===!0?"":t)})})}function Es(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(e){var s=t.elements[e],o=t.attributes[e]||{},i=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]),a=i.reduce(function(e,t){return e[t]="",e},{});if(!l(s)||!f(s))return;Object.assign(s.style,a),Object.keys(o).forEach(function(e){s.removeAttribute(e)})})}}const Ze={name:"applyStyles",enabled:!0,phase:"write",fn:Ss,effect:Es,requires:["computeStyles"]};function m(e){return e.split("-")[0]}O=Math.max,se=Math.min,z=Math.round;function nt(){var e=navigator.userAgentData;return e!=null&&e.brands?e.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function jn(){return!/^((?!chrome|android).)*safari/i.test(nt())}function X(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!1),s=e.getBoundingClientRect(),o=1,i=1,t&&l(e)&&(o=e.offsetWidth>0?z(s.width)/e.offsetWidth||1:1,i=e.offsetHeight>0?z(s.height)/e.offsetHeight||1:1);var s,o,i,f=D(e)?r(e):window,a=f.visualViewport,u=!jn()&&n,c=(s.left+(u&&a?a.offsetLeft:0))/o,d=(s.top+(u&&a?a.offsetTop:0))/i,h=s.width/o,m=s.height/i;return{width:h,height:m,top:d,right:c+h,bottom:d+m,left:c,x:c,y:d}}function dt(e){var t=X(e),n=e.offsetWidth,s=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-s)<=1&&(s=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:s}}function wn(e,t){var n,s=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(s&&Ye(s)){n=t;do{if(n&&e.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function p(e){return r(e).getComputedStyle(e)}function xs(e){return["table","td","th"].indexOf(f(e))>=0}function _(e){return((D(e)?e.ownerDocument:e.document)||window.document).documentElement}function ve(e){return f(e)==="html"?e:e.assignedSlot||e.parentNode||(Ye(e)?e.host:null)||_(e)}function kn(e){return!l(e)||p(e).position==="fixed"?null:e.offsetParent}function _s(e){var t,n,o,s=/firefox/i.test(nt()),i=/Trident/i.test(nt());if(i&&l(e)&&(o=p(e),o.position==="fixed"))return null;for(t=ve(e),Ye(t)&&(t=t.host);l(t)&&["html","body"].indexOf(f(t))<0;){if(n=p(t),n.transform!=="none"||n.perspective!=="none"||n.contain==="paint"||["transform","perspective"].indexOf(n.willChange)!==-1||s&&n.willChange==="filter"||s&&n.filter&&n.filter!=="none")return t;t=t.parentNode}return null}function re(e){for(var n=r(e),t=kn(e);t&&xs(t)&&p(t).position==="static";)t=kn(t);return t&&(f(t)==="html"||f(t)==="body"&&p(t).position==="static")?n:t||_s(e)||n}function Ue(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function oe(e,t,n){return O(e,se(t,n))}function js(e,t,n){var s=oe(e,t,n);return s>n?n:s}function zn(){return{top:0,right:0,bottom:0,left:0}}function Dn(e){return Object.assign({},zn(),e)}function Nn(e,t){return t.reduce(function(t,n){return t[n]=e,t},{})}Ln=function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,Dn(typeof t!="number"?t:Nn(t,G))};function bs(e){var r,c,d,u,p,g,v,b,j,y,_,O,x,C,E,t=e.state,S=e.name,A=e.options,h=t.elements.arrow,f=t.modifiersData.popperOffsets,w=m(t.placement),a=Ue(w),k=[n,i].indexOf(w)>=0,l=k?"height":"width";if(!h||!f)return;g=Ln(A.padding,t),v=dt(h),b=a==="y"?s:n,j=a==="y"?o:i,y=t.rects.reference[l]+t.rects.reference[a]-f[a]-t.rects.popper[l],_=f[a]-t.rects.reference[a],c=re(h),p=c?a==="y"?c.clientHeight||0:c.clientWidth||0:0,O=y/2-_/2,x=g[b],C=p-v[l]-g[j],u=p/2-v[l]/2+O,d=oe(x,u,C),E=a,t.modifiersData[S]=(r={},r[E]=d,r.centerOffset=d-u,r)}function vs(e){var n=e.state,o=e.options,s=o.element,t=s===void 0?"[data-popper-arrow]":s;if(t==null)return;if(typeof t=="string"&&(t=n.elements.popper.querySelector(t),!t))return;if(!wn(n.elements.popper,t))return;n.elements.arrow=t}const Hn={name:"arrow",enabled:!0,phase:"main",fn:bs,effect:vs,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Q(e){return e.split("-")[1]}Bn={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ms(e){var n=e.x,s=e.y,o=window,t=o.devicePixelRatio||1;return{x:z(n*t)/t||0,y:z(s*t)/t||0}}function $n(e){var c,u,h,b,j,y,x,T,z,f=e.popper,D=e.popperRect,d=e.placement,k=e.variation,m=e.offsets,S=e.position,v=e.gpuAcceleration,A=e.adaptive,w=e.roundOffsets,L=e.isFixed,N=m.x,t=N===void 0?0:N,M=m.y,a=M===void 0?0:M,E=typeof w=="function"?w({x:t,y:a}):{x:t,y:a},t=E.x,a=E.y,F=m.hasOwnProperty("x"),C=m.hasOwnProperty("y"),O=n,g=s,l=window;return A&&(c=re(f),y="clientHeight",j="clientWidth",c===r(f)&&(c=_(f),p(c).position!=="static"&&S==="absolute"&&(y="scrollHeight",j="scrollWidth")),c=c,(d===s||(d===n||d===i)&&k===W)&&(g=o,T=L&&c===l&&l.visualViewport?l.visualViewport.height:c[y],a-=T-D.height,a*=v?1:-1),(d===n||(d===s||d===o)&&k===W)&&(O=i,z=L&&c===l&&l.visualViewport?l.visualViewport.width:c[j],t-=z-D.width,t*=v?1:-1)),x=Object.assign({position:S},A&&Bn),b=w===!0?ms({x:t,y:a}):{x:t,y:a},t=b.x,a=b.y,v?Object.assign({},x,(h={},h[g]=C?"0":"",h[O]=F?"0":"",h.transform=(l.devicePixelRatio||1)<=1?"translate("+t+"px, "+a+"px)":"translate3d("+t+"px, "+a+"px, 0)",h)):Object.assign({},x,(u={},u[g]=C?a+"px":"",u[O]=F?t+"px":"",u.transform="",u))}function hs(e){var t=e.state,n=e.options,s=n.gpuAcceleration,c=s===void 0||s,o=n.adaptive,l=o===void 0||o,i=n.roundOffsets,a=i===void 0||i,r={placement:m(t.placement),variation:Q(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:c,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,$n(Object.assign({},r,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:l,roundOffsets:a})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,$n(Object.assign({},r,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:a})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}const Te={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:hs,data:{}};ae={passive:!0};function ls(e){var n=e.state,t=e.instance,s=e.options,o=s.scroll,i=o===void 0||o,a=s.resize,c=a===void 0||a,l=r(n.elements.popper),d=[].concat(n.scrollParents.reference,n.scrollParents.popper);return i&&d.forEach(function(e){e.addEventListener("scroll",t.update,ae)}),c&&l.addEventListener("resize",t.update,ae),function(){i&&d.forEach(function(e){e.removeEventListener("scroll",t.update,ae)}),c&&l.removeEventListener("resize",t.update,ae)}}const Pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:ls,data:{}};Gn={left:"right",right:"left",bottom:"top",top:"bottom"};function ke(e){return e.replace(/left|right|bottom|top/g,function(e){return Gn[e]})}Qn={start:"end",end:"start"};function Zn(e){return e.replace(/start|end/g,function(e){return Qn[e]})}function Ke(e){var t=r(e),n=t.pageXOffset,s=t.pageYOffset;return{scrollLeft:n,scrollTop:s}}function qe(e){return X(_(e)).left+Ke(e).scrollLeft}function rs(e,t){var s,d=r(e),o=_(e),n=d.visualViewport,i=o.clientWidth,a=o.clientHeight,c=0,l=0;return n&&(i=n.width,a=n.height,s=jn(),(s||!s&&t==="fixed")&&(c=n.offsetLeft,l=n.offsetTop)),{width:i,height:a,x:c+qe(e),y:l}}function Fi(e){var s,n=_(e),o=Ke(e),t=(s=e.ownerDocument)==null?void 0:s.body,i=O(n.scrollWidth,n.clientWidth,t?t.scrollWidth:0,t?t.clientWidth:0),r=O(n.scrollHeight,n.clientHeight,t?t.scrollHeight:0,t?t.clientHeight:0),a=-o.scrollLeft+qe(e),c=-o.scrollTop;return p(t||n).direction==="rtl"&&(a+=O(n.clientWidth,t?t.clientWidth:0)-i),{width:i,height:r,x:a,y:c}}function at(e){var t=p(e),n=t.overflow,s=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+s)}function zt(e){return["html","body","#document"].indexOf(f(e))>=0?e.ownerDocument.body:l(e)&&at(e)?e:zt(ve(e))}function ce(e,t){t===void 0&&(t=[]);var s,n=zt(e),o=n===((s=e.ownerDocument)==null?void 0:s.body),i=r(n),a=o?[i].concat(i.visualViewport||[],at(n)?n:[]):n,c=t.concat(a);return o?c:c.concat(ce(ve(a)))}function Fe(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function cs(e,t){var n=X(e,!1,t==="fixed");return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}function qn(e,t,n){return t===ze?Fe(rs(e,n)):D(t)?cs(t,n):Fe(Fi(_(e)))}function ds(e){var n=ce(ve(e)),s=["absolute","fixed"].indexOf(p(e).position)>=0,t=s&&l(e)?re(e):e;return D(t)?n.filter(function(e){return D(e)&&wn(e,t)&&f(e)!=="body"}):[]}function us(e,t,n,s){var a=t==="clippingParents"?ds(e):[].concat(t),i=[].concat(a,[n]),r=i[0],o=i.reduce(function(t,n){var o=qn(e,n,s);return t.top=O(o.top,t.top),t.right=se(o.right,t.right),t.bottom=se(o.bottom,t.bottom),t.left=O(o.left,t.left),t},qn(e,r,s));return o.width=o.right-o.left,o.height=o.bottom-o.top,o.x=o.left,o.y=o.top,o}function Wn(e){var a,r,l,t=e.reference,c=e.element,d=e.placement,u=d?m(d):null,p=d?Q(d):null,h=t.x+t.width/2-c.width/2,f=t.y+t.height/2-c.height/2;switch(u){case s:a={x:h,y:t.y-c.height};break;case o:a={x:h,y:t.y+t.height};break;case i:a={x:t.x+t.width,y:f};break;case n:a={x:t.x-c.width,y:f};break;default:a={x:t.x,y:t.y}}if(r=u?Ue(u):null,r!=null)switch(l=r==="y"?"height":"width",p){case F:a[r]=a[r]-(t[l]/2-c[l]/2);break;case W:a[r]=a[r]+(t[l]/2-c[l]/2);break}return a}function I(e,t){t===void 0&&(t={});var w,n=t,v=n.placement,j=v===void 0?e.placement:v,f=n.strategy,T=f===void 0?e.strategy:f,p=n.boundary,E=p===void 0?$t:p,x=n.rootBoundary,F=x===void 0?ze:x,C=n.elementContext,c=C===void 0?L:C,m=n.altBoundary,M=m!==void 0&&m,b=n.padding,d=b===void 0?0:b,a=Dn(typeof d!="number"?d:Nn(d,G)),S=c===L?Kt:L,O=e.rects.popper,h=e.elements[M?S:c],r=us(D(h)?h:h.contextElement||_(e.elements.popper),E,F,T),y=X(e.elements.reference),k=Wn({reference:y,element:O,strategy:"absolute",placement:j}),A=Fe(Object.assign({},O,k)),l=c===L?A:y,u={top:r.top-l.top+a.top,bottom:l.bottom-r.bottom+a.bottom,left:r.left-l.left+a.left,right:l.right-r.right+a.right},g=e.modifiersData.offset;return c===L&&g&&(w=g[j],Object.keys(u).forEach(function(e){var t=[i,o].indexOf(e)>=0?1:-1,n=[s,o].indexOf(e)>=0?"y":"x";u[e]+=w[n]*t})),u}function fs(e,t){t===void 0&&(t={});var s,n=t,c=n.placement,l=n.boundary,d=n.rootBoundary,u=n.padding,h=n.flipVariations,i=n.allowedAutoPlacements,f=i===void 0?Be:i,a=Q(c),r=a?h?Ie:Ie.filter(function(e){return Q(e)===a}):G,o=r.filter(function(e){return f.indexOf(e)>=0});return o.length===0&&(o=r),s=o.reduce(function(t,n){return t[n]=I(e,{placement:n,boundary:l,rootBoundary:d,padding:u})[m(n)],t},{}),Object.keys(s).sort(function(e,t){return s[e]-s[t]})}function ps(e){if(m(e)===xe)return[];var t=ke(e);return[Zn(e),t,Zn(t)]}function gs(e){var r,c,l,u,h,g,v,y,_,x,E,k,z,t=e.state,a=e.options,C=e.name;if(t.modifiersData[C]._skip)return;for(var M=a.mainAxis,H=M===void 0||M,D=a.altAxis,R=D===void 0||D,L=a.fallbackPlacements,N=a.padding,w=a.boundary,O=a.rootBoundary,B=a.altBoundary,T=a.flipVariations,j=T===void 0||T,V=a.allowedAutoPlacements,d=t.options.placement,U=m(d),P=U===d,K=L||(P||!j?[ke(d)]:ps(d)),p=[d].concat(K).reduce(function(e,n){return e.concat(m(n)===xe?fs(t,{placement:n,boundary:w,rootBoundary:O,padding:N,flipVariations:j,allowedAutoPlacements:V}):n)},[]),W=t.rects.reference,$=t.rects.popper,A=new Map,S=!0,f=p[0],b=0;b=0,_=y?"width":"height",h=I(t,{placement:r,boundary:w,rootBoundary:O,altBoundary:B,padding:N}),l=y?g?i:n:g?o:s,W[_]>$[_]&&(l=ke(l)),z=ke(l),c=[],H&&c.push(h[v]<=0),R&&c.push(h[l]<=0,h[z]<=0),c.every(function(e){return e})){f=r,S=!1;break}A.set(r,c)}if(S)for(k=j?3:1,E=function(t){var n=p.find(function(e){var n=A.get(e);if(n)return n.slice(0,t).every(function(e){return e})});if(n)return f=n,"break"},u=k;u>0;u--)if(x=E(u),x==="break")break;t.placement!==f&&(t.modifiersData[C]._skip=!0,t.placement=f,t.reset=!0)}const Pn={name:"flip",enabled:!0,phase:"main",fn:gs,requiresIfExists:["offset"],data:{_skip:!1}};function Rn(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function Tn(e){return[s,i,o,n].some(function(t){return e[t]>=0})}function ys(e){var t=e.state,a=e.name,r=t.rects.reference,c=t.rects.popper,l=t.modifiersData.preventOverflow,d=I(t,{elementContext:"reference"}),u=I(t,{altBoundary:!0}),n=Rn(d,r),s=Rn(u,c,l),o=Tn(n),i=Tn(s);t.modifiersData[a]={referenceClippingOffsets:n,popperEscapeOffsets:s,isReferenceHidden:o,hasPopperEscaped:i},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":o,"data-popper-escaped":i})}const An={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:ys};function ws(e,t,o){var c=m(e),d=[n,s].indexOf(c)>=0?-1:1,l=typeof o=="function"?o(Object.assign({},t,{placement:e})):o,a=l[0],r=l[1],a=a||0,r=(r||0)*d;return[n,i].indexOf(c)>=0?{x:r,y:a}:{x:a,y:r}}function Os(e){var t=e.state,i=e.options,a=e.name,n=i.offset,r=n===void 0?[0,0]:n,s=Be.reduce(function(e,n){return e[n]=ws(n,t.rects,r),e},{}),o=s[t.placement],c=o.x,l=o.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=c,t.modifiersData.popperOffsets.y+=l),t.modifiersData[a]=s}const xn={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Os};function Cs(e){var t=e.state,n=e.name;t.modifiersData[n]=Wn({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}const Qe={name:"popperOffsets",enabled:!0,phase:"read",fn:Cs,data:{}};function ks(e){return e==="x"?"y":"x"}function As(e){var r,c,h,p,v,w,C,k,A,M,T,z,D,L,R,P,H,B,V,$,W,U,K,q,Y,G,X,Z,t=e.state,l=e.options,be=e.name,fe,ue,ee,te,ie,ce,le,me,pe=l.mainAxis,ge=pe===void 0||pe,ne=l.altAxis,we=ne!==void 0&&ne,_e=l.boundary,ye=l.rootBoundary,ve=l.altBoundary,je=l.padding,de=l.tether,d=de===void 0||de,ae=l.tetherOffset,S=ae===void 0?0:ae,x=I(t,{boundary:_e,rootBoundary:ye,padding:je,altBoundary:ve}),J=m(t.placement),E=Q(t.placement),he=!E,a=Ue(J),j=ks(a),b=t.modifiersData.popperOffsets,u=t.rects.reference,g=t.rects.popper,_=typeof S=="function"?S(Object.assign({},t.rects,{placement:t.placement})):S,f=typeof _=="number"?{mainAxis:_,altAxis:_}:Object.assign({mainAxis:0,altAxis:0},_),y=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,N={x:0,y:0};if(!b)return;ge&&(R=a==="y"?s:n,P=a==="y"?o:i,r=a==="y"?"height":"width",h=b[a],V=h+x[R],$=h-x[P],W=d?-g[r]/2:0,Z=E===F?u[r]:g[r],X=E===F?-g[r]:-u[r],q=t.elements.arrow,ue=d&&q?dt(q):{width:0,height:0},k=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:zn(),K=k[R],U=k[P],v=oe(0,u[r],ue[r]),ee=he?u[r]/2-W-v-K-f.mainAxis:Z-v-K-f.mainAxis,te=he?-u[r]/2+W+v+U+f.mainAxis:X+v+U+f.mainAxis,C=t.elements.arrow&&re(t.elements.arrow),ie=C?a==="y"?C.clientTop||0:C.clientLeft||0:0,B=(fe=y?.[a])!=null?fe:0,ce=h+ee-B-ie,le=h+te-B,H=oe(d?se(V,ce):V,h,d?O($,le):$),b[a]=H,N[a]=H-h),we&&(Y=a==="x"?s:n,me=a==="x"?o:i,c=b[j],p=j==="y"?"height":"width",L=c+x[Y],D=c-x[me],w=[s,n].indexOf(J)!==-1,z=(G=y?.[j])!=null?G:0,T=w?L:c-u[p]-g[p]-z+f.altAxis,M=w?c+u[p]+g[p]-z-f.altAxis:D,A=d&&w?js(T,c,M):oe(d?T:L,c,d?M:D),b[j]=A,N[j]=A-c),t.modifiersData[be]=N}const un={name:"preventOverflow",enabled:!0,phase:"main",fn:As,requiresIfExists:["offset"]};function Ms(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}function Fs(e){return e===r(e)||!l(e)?Ke(e):Ms(e)}function Ts(e){var t=e.getBoundingClientRect(),n=z(t.width)/e.offsetWidth||1,s=z(t.height)/e.offsetHeight||1;return n!==1||s!==1}function zs(e,t,n){n===void 0&&(n=!1);var r=l(t),c=l(t)&&Ts(t),i=_(t),o=X(e,c,n),a={scrollLeft:0,scrollTop:0},s={x:0,y:0};return(r||!r&&!n)&&((f(t)!=="body"||at(i))&&(a=Fs(t)),l(t)?(s=X(t,!0),s.x+=t.clientLeft,s.y+=t.clientTop):i&&(s.x=qe(i))),{x:o.left+a.scrollLeft-s.x,y:o.top+a.scrollTop-s.y,width:o.width,height:o.height}}function Ds(e){var n=new Map,t=new Set,s=[];e.forEach(function(e){n.set(e.name,e)});function o(e){t.add(e.name);var i=[].concat(e.requires||[],e.requiresIfExists||[]);i.forEach(function(e){if(!t.has(e)){var s=n.get(e);s&&o(s)}}),s.push(e)}return e.forEach(function(e){t.has(e.name)||o(e)}),s}function Ns(e){var t=Ds(e);return on.reduce(function(e,n){return e.concat(t.filter(function(e){return e.phase===n}))},[])}function Ls(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}function Rs(e){var t=e.reduce(function(e,t){var n=e[t.name];return e[t.name]=n?Object.assign({},n,t,{options:Object.assign({},n.options,t.options),data:Object.assign({},n.data,t.data)}):t,e},{});return Object.keys(t).map(function(e){return t[e]})}Je={placement:"bottom",modifiers:[],strategy:"absolute"};function Tt(){for(var t=arguments.length,n=new Array(t),e=0;eNumber.parseInt(e,10)):typeof e=="function"?t=>e(t,this._element):e}_getPopperConfig(){const e={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||this._config.display==="static")&&(b.setDataAttribute(this._menu,"popper","static"),e.modifiers=[{name:"applyStyles",enabled:!1}]),{...e,...typeof this._config.popperConfig=="function"?this._config.popperConfig(e):this._config.popperConfig}}_selectMenuItem({key:e,target:n}){const s=t.find(_o,this._menu).filter(e=>H(e));if(!s.length)return;Le(s,n,e===gt,!s.includes(n)).focus()}static jQueryInterface(e){return this.each(function(){const t=h.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}static clearMenus(e){if(e.button===to||e.type==="keyup"&&e.key!==vt)return;const n=t.find(vo);for(const a of n){const t=h.getInstance(a);if(!t||t._config.autoClose===!1)continue;const s=e.composedPath(),o=s.includes(t._menu);if(s.includes(t._element)||t._config.autoClose==="inside"&&!o||t._config.autoClose==="outside"&&o)continue;if(t._menu.contains(e.target)&&(e.type==="keyup"&&e.key===vt||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;const i={relatedTarget:t._element};e.type==="click"&&(i.clickEvent=e),t._completeHide(i)}}static dataApiKeydownHandler(e){const a=/input|textarea/i.test(e.target.tagName),s=e.key===Qs,o=[Js,gt].includes(e.key);if(!o&&!s)return;if(a&&!s)return;e.preventDefault();const i=this.matches(S)?this:t.prev(this,S)[0]||t.next(this,S)[0]||t.findOne(S,e.delegateTarget.parentNode),n=h.getOrCreateInstance(i);if(o){e.stopPropagation(),n.show(),n._selectMenuItem(e);return}n._isShown()&&(e.stopPropagation(),n.hide(),i.focus())}}e.on(document,jt,S,h.dataApiKeydownHandler),e.on(document,jt,be,h.dataApiKeydownHandler),e.on(document,bt,h.clearMenus),e.on(document,co,h.clearMenus),e.on(document,bt,S,function(e){e.preventDefault(),h.getOrCreateInstance(this).toggle()}),c(h);const wt=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ot=".sticky-top",me="padding-right",xt="margin-right";class tt{constructor(){this._element=document.body}getWidth(){const e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){const e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,me,t=>t+e),this._setElementAttributes(wt,me,t=>t+e),this._setElementAttributes(Ot,xt,t=>t-e)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,me),this._resetElementAttributes(wt,me),this._resetElementAttributes(Ot,xt)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(e,t,n){const s=this.getWidth(),o=e=>{if(e!==this._element&&window.innerWidth>e.clientWidth+s)return;this._saveInitialAttribute(e,t);const o=window.getComputedStyle(e).getPropertyValue(t);e.style.setProperty(t,`${n(Number.parseFloat(o))}px`)};this._applyManipulationCallback(e,o)}_saveInitialAttribute(e,t){const n=e.style.getPropertyValue(t);n&&b.setDataAttribute(e,t,n)}_resetElementAttributes(e,t){const n=e=>{const n=b.getDataAttribute(e,t);if(n===null){e.style.removeProperty(t);return}b.removeDataAttribute(e,t),e.style.setProperty(t,n)};this._applyManipulationCallback(e,n)}_applyManipulationCallback(e,n){if(g(e)){n(e);return}for(const s of t.find(e,this._element))n(s)}}const Ft="backdrop",Ho="fade",pt="show",Dt=`mousedown.bs.${Ft}`,Vo={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},$o={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Nt extends te{constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return Vo}static get DefaultType(){return $o}static get NAME(){return Ft}show(e){if(!this._config.isVisible){y(e);return}this._append();const t=this._getElement();this._config.isAnimated&&ne(t),t.classList.add(pt),this._emulateAnimation(()=>{y(e)})}hide(e){if(!this._config.isVisible){y(e);return}this._getElement().classList.remove(pt),this._emulateAnimation(()=>{this.dispose(),y(e)})}dispose(){if(!this._isAppended)return;e.off(this._element,Dt),this._element.remove(),this._isAppended=!1}_getElement(){if(!this._element){const e=document.createElement("div");e.className=this._config.className,this._config.isAnimated&&e.classList.add(Ho),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=E(e.rootElement),e}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),e.on(t,Dt,()=>{y(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(e){Yn(e,this._getElement(),this._config.isAnimated)}}const Uo="focustrap",Ko="bs.focustrap",_e=`.${Ko}`,Yo=`focusin${_e}`,Go=`keydown.tab${_e}`,Xo="Tab",Qo="forward",Rt="backward",Jo={autofocus:!0,trapElement:null},ei={autofocus:"boolean",trapElement:"element"};class Pt extends te{constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Jo}static get DefaultType(){return ei}static get NAME(){return Uo}activate(){if(this._isActive)return;this._config.autofocus&&this._config.trapElement.focus(),e.off(document,_e),e.on(document,Yo,e=>this._handleFocusin(e)),e.on(document,Go,e=>this._handleKeydown(e)),this._isActive=!0}deactivate(){if(!this._isActive)return;this._isActive=!1,e.off(document,_e)}_handleFocusin(e){const{trapElement:n}=this._config;if(e.target===document||e.target===n||n.contains(e.target))return;const s=t.focusableChildren(n);s.length===0?n.focus():this._lastTabNavDirection===Rt?s[s.length-1].focus():s[0].focus()}_handleKeydown(e){if(e.key!==Xo)return;this._lastTabNavDirection=e.shiftKey?Rt:Qo}}const ni="modal",si="bs.modal",d=`.${si}`,ii=".data-api",ai="Escape",ri=`hide${d}`,ci=`hidePrevented${d}`,Ht=`hidden${d}`,It=`show${d}`,ui=`shown${d}`,hi=`resize${d}`,mi=`click.dismiss${d}`,fi=`mousedown.dismiss${d}`,pi=`keydown.dismiss${d}`,gi=`click${d}${ii}`,Bt="modal-open",bi="fade",Vt="show",Ne="modal-static",_i=".modal.show",wi=".modal-dialog",Oi=".modal-body",xi='[data-bs-toggle="modal"]',Ci={backdrop:!0,focus:!0,keyboard:!0},Ei={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class B extends u{constructor(e,n){super(e,n),this._dialog=t.findOne(wi,this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new tt,this._addEventListeners()}static get Default(){return Ci}static get DefaultType(){return Ei}static get NAME(){return ni}toggle(e){return this._isShown?this.hide():this.show(e)}show(t){if(this._isShown||this._isTransitioning)return;const n=e.trigger(this._element,It,{relatedTarget:t});if(n.defaultPrevented)return;this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Bt),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t))}hide(){if(!this._isShown||this._isTransitioning)return;const t=e.trigger(this._element,ri);if(t.defaultPrevented)return;this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Vt),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())}dispose(){for(const t of[window,this._dialog])e.off(t,d);this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Nt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Pt({trapElement:this._element})}_showElement(n){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const s=t.findOne(Oi,this._dialog);s&&(s.scrollTop=0),ne(this._element),this._element.classList.add(Vt);const o=()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,e.trigger(this._element,ui,{relatedTarget:n})};this._queueCallback(o,this._dialog,this._isAnimated())}_addEventListeners(){e.on(this._element,pi,e=>{if(e.key!==ai)return;if(this._config.keyboard){e.preventDefault(),this.hide();return}this._triggerBackdropTransition()}),e.on(window,hi,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),e.on(this._element,fi,t=>{e.one(this._element,mi,e=>{if(this._element!==t.target||this._element!==e.target)return;if(this._config.backdrop==="static"){this._triggerBackdropTransition();return}this._config.backdrop&&this.hide()})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Bt),this._resetAdjustments(),this._scrollBar.reset(),e.trigger(this._element,Ht)})}_isAnimated(){return this._element.classList.contains(bi)}_triggerBackdropTransition(){const n=e.trigger(this._element,ci);if(n.defaultPrevented)return;const s=this._element.scrollHeight>document.documentElement.clientHeight,t=this._element.style.overflowY;if(t==="hidden"||this._element.classList.contains(Ne))return;s||(this._element.style.overflowY="hidden"),this._element.classList.add(Ne),this._queueCallback(()=>{this._element.classList.remove(Ne),this._queueCallback(()=>{this._element.style.overflowY=t},this._dialog)},this._dialog),this._element.focus()}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),n=e>0;if(n&&!t){const t=a()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!n&&t){const t=a()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(e,t){return this.each(function(){const n=B.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof n[e]=="undefined")throw new TypeError(`No method named "${e}"`);n[e](t)})}}e.on(document,gi,xi,function(n){const s=v(this);["A","AREA"].includes(this.tagName)&&n.preventDefault(),e.one(s,It,t=>{if(t.defaultPrevented)return;e.one(s,Ht,()=>{H(this)&&this.focus()})});const o=t.findOne(_i);o&&B.getInstance(o).hide();const i=B.getOrCreateInstance(s);i.toggle(this)}),Se(B),c(B);const Ai="offcanvas",Si="bs.offcanvas",j=`.${Si}`,Ut=".data-api",Ti=`load${j}${Ut}`,zi="Escape",qt="show",Yt="showing",an="hiding",Ri="offcanvas-backdrop",rn=".offcanvas.show",Hi=`show${j}`,Ii=`shown${j}`,Bi=`hide${j}`,cn=`hidePrevented${j}`,ln=`hidden${j}`,Wi=`resize${j}`,Ui=`click${j}${Ut}`,Ki=`keydown.dismiss${j}`,qi='[data-bs-toggle="offcanvas"]',Yi={backdrop:!0,keyboard:!0,scroll:!1},Gi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class A extends u{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Yi}static get DefaultType(){return Gi}static get NAME(){return Ai}toggle(e){return this._isShown?this.hide():this.show(e)}show(t){if(this._isShown)return;const n=e.trigger(this._element,Hi,{relatedTarget:t});if(n.defaultPrevented)return;this._isShown=!0,this._backdrop.show(),this._config.scroll||(new tt).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Yt);const s=()=>{(!this._config.scroll||this._config.backdrop)&&this._focustrap.activate(),this._element.classList.add(qt),this._element.classList.remove(Yt),e.trigger(this._element,Ii,{relatedTarget:t})};this._queueCallback(s,this._element,!0)}hide(){if(!this._isShown)return;const t=e.trigger(this._element,Bi);if(t.defaultPrevented)return;this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(an),this._backdrop.hide();const n=()=>{this._element.classList.remove(qt,an),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new tt).reset(),e.trigger(this._element,ln)};this._queueCallback(n,this._element,!0)}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const n=()=>{if(this._config.backdrop==="static"){e.trigger(this._element,cn);return}this.hide()},t=Boolean(this._config.backdrop);return new Nt({className:Ri,isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?n:null})}_initializeFocusTrap(){return new Pt({trapElement:this._element})}_addEventListeners(){e.on(this._element,Ki,t=>{if(t.key!==zi)return;if(!this._config.keyboard){e.trigger(this._element,cn);return}this.hide()})}static jQueryInterface(e){return this.each(function(){const t=A.getOrCreateInstance(this,e);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)})}}e.on(document,Ui,qi,function(n){const s=v(this);if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),w(this))return;e.one(s,ln,()=>{H(this)&&this.focus()});const o=t.findOne(rn);o&&o!==s&&A.getInstance(o).hide();const i=A.getOrCreateInstance(s);i.toggle(this)}),e.on(window,Ti,()=>{for(const e of t.find(rn))A.getOrCreateInstance(e).show()}),e.on(window,Wi,()=>{for(const e of t.find("[aria-modal][class*=show][class*=offcanvas-]"))getComputedStyle(e).position!=="fixed"&&A.getOrCreateInstance(e).hide()}),Se(A),c(A);const Qi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Zi=/^aria-[\w-]*$/i,Ji=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,ea=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,ta=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!Qi.has(n)||Boolean(Ji.test(e.nodeValue)||ea.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))},dn={"*":["class","dir","id","lang","role",Zi],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]};function sa(e,t,n){if(!e.length)return e;if(n&&typeof n=="function")return n(e);const o=new window.DOMParser,s=o.parseFromString(e,"text/html"),i=[].concat(...s.body.querySelectorAll("*"));for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[].concat(...e.attributes),o=[].concat(t["*"]||[],t[n]||[]);for(const t of s)ta(t,o)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const oa="TemplateFactory",ia={allowList:dn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
      "},aa={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ra={entry:"(string|element|function|null)",selector:"(string|element)"};class ca extends te{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return ia}static get DefaultType(){return aa}static get NAME(){return oa}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},ra)}_setContent(e,n,s){const o=t.findOne(s,e);if(!o)return;if(n=this._resolvePossibleFunction(n),!n){o.remove();return}if(g(n)){this._putElementInTemplate(E(n),o);return}if(this._config.html){o.innerHTML=this._maybeSanitize(n);return}o.textContent=n}_maybeSanitize(e){return this._config.sanitize?sa(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return typeof e=="function"?e(this):e}_putElementInTemplate(e,t){if(this._config.html){t.innerHTML="",t.append(e);return}t.textContent=e.textContent}}const la="tooltip",da=new Set(["sanitize","allowList","sanitizeFn"]),Xe="fade",ha="modal",ue="show",fa=".tooltip-inner",hn=`.${ha}`,mn="hide.bs.modal",J="hover",ot="focus",ja="click",ya="manual",_a="hide",wa="hidden",Oa="show",xa="shown",Ca="inserted",Ea="click",ka="focusin",Aa="focusout",Sa="mouseenter",Ma="mouseleave",Fa={AUTO:"auto",TOP:"top",RIGHT:a()?"left":"right",BOTTOM:"bottom",LEFT:a()?"right":"left"},Ta={allowList:dn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},za={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class U extends u{constructor(e,t){if(typeof _t=="undefined")throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Ta}static get DefaultType(){return za}static get NAME(){return la}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){if(!this._isEnabled)return;if(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()){this._leave();return}this._enter()}dispose(){clearTimeout(this._timeout),e.off(this._element.closest(hn),mn,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if(this._element.style.display==="none")throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const n=e.trigger(this._element,this.constructor.eventName(Oa)),s=Jn(this._element),o=(s||this._element.ownerDocument.documentElement).contains(this._element);if(n.defaultPrevented||!o)return;this._disposePopper();const t=this._getTipElement();this._element.setAttribute("aria-describedby",t.getAttribute("id"));const{container:i}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(i.append(t),e.trigger(this._element,this.constructor.eventName(Ca))),this._popper=this._createPopper(t),t.classList.add(ue),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))e.on(t,"mouseover",Oe);const a=()=>{e.trigger(this._element,this.constructor.eventName(xa)),this._isHovered===!1&&this._leave(),this._isHovered=!1};this._queueCallback(a,this.tip,this._isAnimated())}hide(){if(!this._isShown())return;const t=e.trigger(this._element,this.constructor.eventName(_a));if(t.defaultPrevented)return;const n=this._getTipElement();if(n.classList.remove(ue),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))e.off(t,"mouseover",Oe);this._activeTrigger[ja]=!1,this._activeTrigger[ot]=!1,this._activeTrigger[J]=!1,this._isHovered=null;const s=()=>{if(this._isWithActiveTrigger())return;this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),e.trigger(this._element,this.constructor.eventName(wa))};this._queueCallback(s,this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(Xe,ue),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=Gr(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add(Xe),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new ca({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[fa]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Xe)}_isShown(){return this.tip&&this.tip.classList.contains(ue)}_createPopper(e){const t=typeof this._config.placement=="function"?this._config.placement.call(this,e,this._element):this._config.placement,n=Fa[t.toUpperCase()];return he(this._element,e,this._getPopperConfig(n))}_getOffset(){const{offset:e}=this._config;return typeof e=="string"?e.split(",").map(e=>Number.parseInt(e,10)):typeof e=="function"?t=>e(t,this._element):e}_resolvePossibleFunction(e){return typeof e=="function"?e.call(this._element):e}_getPopperConfig(e){const t={placement:e,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:e=>{this._getTipElement().setAttribute("data-popper-placement",e.state.placement)}}]};return{...t,...typeof this._config.popperConfig=="function"?this._config.popperConfig(t):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const n of t)if(n==="click")e.on(this._element,this.constructor.eventName(Ea),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t.toggle()});else if(n!==ya){const t=n===J?this.constructor.eventName(Sa):this.constructor.eventName(ka),s=n===J?this.constructor.eventName(Ma):this.constructor.eventName(Aa);e.on(this._element,t,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[e.type==="focusin"?ot:J]=!0,t._enter()}),e.on(this._element,s,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[e.type==="focusout"?ot:J]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},e.on(this._element.closest(hn),mn,this._hideModalHandler)}_fixTitle(){const e=this._element.getAttribute("title");if(!e)return;!this._element.getAttribute("aria-label")&&!this._element.textContent.trim()&&this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title")}_enter(){if(this._isShown()||this._isHovered){this._isHovered=!0;return}this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show)}_leave(){if(this._isWithActiveTrigger())return;this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide)}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=b.getDataAttributes(this._element);for(const e of Object.keys(t))da.has(e)&&delete t[e];return e={...t,...typeof e=="object"&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=e.container===!1?document.body:E(e.container),typeof e.delay=="number"&&(e.delay={show:e.delay,hide:e.delay}),typeof e.title=="number"&&(e.title=e.title.toString()),typeof e.content=="number"&&(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const t in this._config)this.constructor.Default[t]!==this._config[t]&&(e[t]=this._config[t]);return e.selector=!1,e.trigger="manual",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each(function(){const t=U.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}}c(U);const Na="popover",La=".popover-header",Ra=".popover-body",Pa={...U.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Ha={...U.DefaultType,content:"(null|string|element|function)"};class ct extends U{static get Default(){return Pa}static get DefaultType(){return Ha}static get NAME(){return Na}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[La]:this._getTitle(),[Ra]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each(function(){const t=ct.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}}c(ct);const Ba="scrollspy",Va="bs.scrollspy",lt=`.${Va}`,Wa=".data-api",Ua=`activate${lt}`,pn=`click${lt}`,qa=`load${lt}${Wa}`,Ya="dropdown-item",q="active",Xa='[data-bs-spy="scroll"]',mt="[href]",Za=".nav, .list-group",gn=".nav-link",er=".nav-item",tr=".list-group-item",nr=`${gn}, ${er} > ${gn}, ${tr}`,sr=".dropdown",or=".dropdown-toggle",ir={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ar={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ee extends u{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=getComputedStyle(this._element).overflowY==="visible"?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ir}static get DefaultType(){return ar}static get NAME(){return Ba}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=E(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,typeof e.threshold=="string"&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_maybeEnableSmoothScroll(){if(!this._config.smoothScroll)return;e.off(this._config.target,pn),e.on(this._config.target,pn,mt,e=>{const t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();const n=this._rootElement||window,s=t.offsetTop-this._element.offsetTop;if(n.scrollTo){n.scrollTo({top:s,behavior:"smooth"});return}n.scrollTop=s}})}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(e=>this._observerCallback(e),e)}_observerCallback(e){const n=e=>this._targetLinks.get(`#${e.target.id}`),s=e=>{this._previousScrollData.visibleEntryTop=e.target.offsetTop,this._process(n(e))},t=(this._rootElement||document.documentElement).scrollTop,o=t>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=t;for(const i of e){if(!i.isIntersecting){this._activeTarget=null,this._clearActiveClass(n(i));continue}const a=i.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(o&&a){if(s(i),!t)return;continue}!o&&!a&&s(i)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const e=t.find(mt,this._config.target);for(const n of e){if(!n.hash||w(n))continue;const s=t.findOne(n.hash,this._element);H(s)&&(this._targetLinks.set(n.hash,n),this._observableSections.set(n.hash,s))}}_process(t){if(this._activeTarget===t)return;this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(q),this._activateParents(t),e.trigger(this._element,Ua,{relatedTarget:t})}_activateParents(e){if(e.classList.contains(Ya)){t.findOne(or,e.closest(sr)).classList.add(q);return}for(const n of t.parents(e,Za))for(const e of t.prev(n,nr))e.classList.add(q)}_clearActiveClass(e){e.classList.remove(q);const n=t.find(`${mt}.${q}`,e);for(const e of n)e.classList.remove(q)}static jQueryInterface(e){return this.each(function(){const t=Ee.getOrCreateInstance(this,e);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()})}}e.on(window,qa,()=>{for(const e of t.find(Xa))Ee.getOrCreateInstance(e)}),c(Ee);const cr="tab",lr="bs.tab",M=`.${lr}`,ur=`hide${M}`,hr=`hidden${M}`,mr=`show${M}`,fr=`shown${M}`,pr=`click${M}`,gr=`keydown${M}`,vr=`load${M}`,br="ArrowLeft",bn="ArrowRight",yr="ArrowUp",yn="ArrowDown",N="active",Mn="fade",We="show",Cr="dropdown",Er=".dropdown-toggle",kr=".dropdown-menu",$e=":not(.dropdown-toggle)",Sr='.list-group, .nav, [role="tablist"]',Mr=".nav-item, .list-group-item",Fr=`.nav-link${$e}, .list-group-item${$e}, [role="tab"]${$e}`,Kn='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',De=`${Fr}, ${Kn}`,Dr=`.${N}[data-bs-toggle="tab"], .${N}[data-bs-toggle="pill"], .${N}[data-bs-toggle="list"]`;class R extends u{constructor(t){if(super(t),this._parent=this._element.closest(Sr),!this._parent)return;this._setInitialAttributes(this._parent,this._getChildren()),e.on(this._element,gr,e=>this._keydown(e))}static get NAME(){return cr}show(){const t=this._element;if(this._elemIsActive(t))return;const n=this._getActiveElem(),s=n?e.trigger(n,ur,{relatedTarget:t}):null,o=e.trigger(t,mr,{relatedTarget:n});if(o.defaultPrevented||s&&s.defaultPrevented)return;this._deactivate(n,t),this._activate(t,n)}_activate(t,n){if(!t)return;t.classList.add(N),this._activate(v(t));const s=()=>{if(t.getAttribute("role")!=="tab"){t.classList.add(We);return}t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),e.trigger(t,fr,{relatedTarget:n})};this._queueCallback(s,t,t.classList.contains(Mn))}_deactivate(t,n){if(!t)return;t.classList.remove(N),t.blur(),this._deactivate(v(t));const s=()=>{if(t.getAttribute("role")!=="tab"){t.classList.remove(We);return}t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),e.trigger(t,hr,{relatedTarget:n})};this._queueCallback(s,t,t.classList.contains(Mn))}_keydown(e){if(![br,bn,yr,yn].includes(e.key))return;e.stopPropagation(),e.preventDefault();const n=[bn,yn].includes(e.key),t=Le(this._getChildren().filter(e=>!w(e)),e.target,n,!0);t&&(t.focus({preventScroll:!0}),R.getOrCreateInstance(t).show())}_getChildren(){return t.find(De,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=v(e);if(!t)return;this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`#${e.id}`)}_toggleDropDown(e,n){const s=this._getOuterElement(e);if(!s.classList.contains(Cr))return;const o=(e,o)=>{const i=t.findOne(e,s);i&&i.classList.toggle(o,n)};o(Er,N),o(kr,We),s.setAttribute("aria-expanded",n)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains(N)}_getInnerElement(e){return e.matches(De)?e:t.findOne(De,e)}_getOuterElement(e){return e.closest(Mr)||e}static jQueryInterface(e){return this.each(function(){const t=R.getOrCreateInstance(this);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()})}}e.on(document,pr,Kn,function(e){if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),w(this))return;R.getOrCreateInstance(this).show()}),e.on(window,vr,()=>{for(const e of t.find(Dr))R.getOrCreateInstance(e)}),c(R);const Lr="toast",Rr="bs.toast",C=`.${Rr}`,Hr=`mouseover${C}`,Ir=`mouseout${C}`,Br=`focusin${C}`,Vr=`focusout${C}`,$r=`hide${C}`,Wr=`hidden${C}`,Ur=`show${C}`,Kr=`shown${C}`,qr="fade",ts="hide",ge="show",we="showing",Qr={animation:"boolean",autohide:"boolean",delay:"number"},Zr={animation:!0,autohide:!0,delay:5e3};class Ce extends u{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Zr}static get DefaultType(){return Qr}static get NAME(){return Lr}show(){const t=e.trigger(this._element,Ur);if(t.defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add(qr);const n=()=>{this._element.classList.remove(we),e.trigger(this._element,Kr),this._maybeScheduleHide()};this._element.classList.remove(ts),ne(this._element),this._element.classList.add(ge,we),this._queueCallback(n,this._element,this._config.animation)}hide(){if(!this.isShown())return;const t=e.trigger(this._element,$r);if(t.defaultPrevented)return;const n=()=>{this._element.classList.add(ts),this._element.classList.remove(we,ge),e.trigger(this._element,Wr)};this._element.classList.add(we),this._queueCallback(n,this._element,this._config.animation)}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ge),super.dispose()}isShown(){return this._element.classList.contains(ge)}_maybeScheduleHide(){if(!this._config.autohide)return;if(this._hasMouseInteraction||this._hasKeyboardInteraction)return;this._timeout=setTimeout(()=>{this.hide()},this._config.delay)}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":{this._hasMouseInteraction=t;break}case"focusin":case"focusout":{this._hasKeyboardInteraction=t;break}}if(t){this._clearTimeout();return}const n=e.relatedTarget;if(this._element===n||this._element.contains(n))return;this._maybeScheduleHide()}_setListeners(){e.on(this._element,Hr,e=>this._onInteraction(e,!0)),e.on(this._element,Ir,e=>this._onInteraction(e,!1)),e.on(this._element,Br,e=>this._onInteraction(e,!0)),e.on(this._element,Vr,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each(function(){const t=Ce.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e](this)}})}}Se(Ce),c(Ce);const ec={Alert:de,Button:fe,Carousel:ie,Collapse:le,Dropdown:h,Modal:B,Offcanvas:A,Popover:ct,ScrollSpy:Ee,Tab:R,Toast:Ce,Tooltip:U};return ec}),function(e){"use strict";e(function(){e('[data-bs-toggle="tooltip"]').tooltip(),e('[data-bs-toggle="popover"]').popover(),e(".popover-dismiss").popover({trigger:"focus"})});function t(e){return e.offset().top+e.outerHeight()}e(function(){var n,o,i,s=e(".js-td-cover");if(!s.length)return;o=t(s),i=e(".js-navbar-scroll").offset().top,n=Math.ceil(e(".js-navbar-scroll").outerHeight()),o-i',t.href="#"+e.id,e.insertAdjacentElement("beforeend",t),e.addEventListener("mouseenter",function(){t.style.visibility="initial"}),e.addEventListener("mouseleave",function(){t.style.visibility="hidden"})}})})}(jQuery),function(e){"use strict";var t={init:function(){e(document).ready(function(){e(document).on("keypress",".td-search input",function(t){if(t.keyCode!==13)return;var n=e(this).val(),s="search/?q="+n;return document.location=s,!1})})}};t.init()}(jQuery) \ No newline at end of file diff --git a/docs/public/js/main.min.5ea1489ff282dac019c3662f41fd60d2e11a7d4b1020f3be5653eda0e5f8cc3a.js b/docs/public/js/main.min.5ea1489ff282dac019c3662f41fd60d2e11a7d4b1020f3be5653eda0e5f8cc3a.js new file mode 100644 index 000000000..6b6f45dbe --- /dev/null +++ b/docs/public/js/main.min.5ea1489ff282dac019c3662f41fd60d2e11a7d4b1020f3be5653eda0e5f8cc3a.js @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.2.3 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */(function(e,t){typeof exports=="object"&&typeof module!="undefined"?module.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis!="undefined"?globalThis:e||self,e.bootstrap=t())})(this,function(){"use strict";const ro=1e6,Jr=1e3,ut="transitionend",Xr=e=>e==null?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),Gr=e=>{do e+=Math.floor(Math.random()*ro);while(document.getElementById(e))return e},ss=e=>{let t=e.getAttribute("data-bs-target");if(!t||t==="#"){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&n!=="#"?n.trim():null}return t},ns=e=>{const t=ss(e);return t?document.querySelector(t)?t:null:null},v=e=>{const t=ss(e);return t?document.querySelector(t):null},Yr=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),o=Number.parseFloat(n);return!s&&!o?0:(t=t.split(",")[0],n=n.split(",")[0],(Number.parseFloat(t)+Number.parseFloat(n))*Jr)},es=e=>{e.dispatchEvent(new Event(ut))},g=e=>!!e&&typeof e=="object"&&(typeof e.jquery!="undefined"&&(e=e[0]),typeof e.nodeType!="undefined"),E=e=>g(e)?e.jquery?e[0]:e:typeof e=="string"&&e.length>0?document.querySelector(e):null,H=e=>{if(!g(e)||e.getClientRects().length===0)return!1;const n=getComputedStyle(e).getPropertyValue("visibility")==="visible",t=e.closest("details:not([open])");if(!t)return n;if(t!==e){const n=e.closest("summary");if(n&&n.parentNode!==t)return!1;if(n===null)return!1}return n},w=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(typeof e.disabled!="undefined"?e.disabled:e.hasAttribute("disabled")&&e.getAttribute("disabled")!=="false"),Jn=e=>{if(!document.documentElement.attachShadow)return null;if(typeof e.getRootNode=="function"){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?Jn(e.parentNode):null},Oe=()=>{},ne=e=>{e.offsetHeight},Xn=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,He=[],Pr=e=>{document.readyState==="loading"?(He.length||document.addEventListener("DOMContentLoaded",()=>{for(const e of He)e()}),He.push(e)):e()},a=()=>document.documentElement.dir==="rtl",c=e=>{Pr(()=>{const t=Xn();if(t){const n=e.NAME,s=t.fn[n];t.fn[n]=e.jQueryInterface,t.fn[n].Constructor=e,t.fn[n].noConflict=()=>(t.fn[n]=s,e.jQueryInterface)}})},y=e=>{typeof e=="function"&&e()},Yn=(e,t,n=!0)=>{if(!n){y(e);return}const i=5,a=Yr(t)+i;let s=!1;const o=({target:n})=>{if(n!==t)return;s=!0,t.removeEventListener(ut,o),y(e)};t.addEventListener(ut,o),setTimeout(()=>{s||es(t)},a)},Le=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return o===-1?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])},Nr=/[^.]*(?=\..*)\.|.*/,zr=/\..*/,Tr=/::\d+$/,Ge={};let Un=1;const Vn={mouseenter:"mouseover",mouseleave:"mouseout"},Ar=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function In(e,t){return t&&`${t}::${Un++}`||e.uidEvent||Un++}function Fn(e){const t=In(e);return e.uidEvent=t,Ge[t]=Ge[t]||{},Ge[t]}function xr(t,n){return function s(o){return st(o,{delegateTarget:t}),s.oneOff&&e.off(t,o.type,n),n.apply(t,[o])}}function Or(t,n,s){return function o(i){const a=t.querySelectorAll(n);for(let{target:r}=i;r&&r!==this;r=r.parentNode)for(const c of a){if(c!==r)continue;return st(i,{delegateTarget:r}),o.oneOff&&e.off(t,i.type,n,s),s.apply(r,[i])}}}function Sn(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function En(e,t,n){const o=typeof t=="string",i=o?n:t||n;let s=On(e);return Ar.has(s)||(s=e),[o,i,s]}function Cn(e,t,n,s,o){if(typeof t!="string"||!e)return;let[r,i,c]=En(t,n,s);if(t in Vn){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};i=e(i)}const d=Fn(e),u=d[c]||(d[c]={}),l=Sn(u,i,r?n:null);if(l){l.oneOff=l.oneOff&&o;return}const h=In(i,t.replace(Nr,"")),a=r?Or(e,n,i):xr(e,i);a.delegationSelector=r?n:null,a.callable=i,a.oneOff=o,a.uidEvent=h,u[h]=a,e.addEventListener(c,a,r)}function Me(e,t,n,s,o){const i=Sn(t[n],s,o);if(!i)return;e.removeEventListener(n,i,Boolean(o)),delete t[n][i.uidEvent]}function wr(e,t,n,s){const o=t[n]||{};for(const i of Object.keys(o))if(i.includes(s)){const s=o[i];Me(e,t,n,s.callable,s.delegationSelector)}}function On(e){return e=e.replace(zr,""),Vn[e]||e}const e={on(e,t,n,s){Cn(e,t,n,s,!1)},one(e,t,n,s){Cn(e,t,n,s,!0)},off(e,t,n,s){if(typeof t!="string"||!e)return;const[c,r,i]=En(t,n,s),l=i!==t,o=Fn(e),a=o[i]||{},d=t.startsWith(".");if(typeof r!="undefined"){if(!Object.keys(a).length)return;Me(e,o,i,r,c?n:null);return}if(d)for(const n of Object.keys(o))wr(e,o,n,t.slice(1));for(const n of Object.keys(a)){const s=n.replace(Tr,"");if(!l||t.includes(s)){const t=a[n];Me(e,o,i,t.callable,t.delegationSelector)}}},trigger(e,t,n){if(typeof t!="string"||!e)return null;const i=Xn(),l=On(t),d=t!==l;let s=null,a=!0,r=!0,c=!1;d&&i&&(s=i.Event(t,n),i(e).trigger(s),a=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),c=s.isDefaultPrevented());let o=new Event(t,{bubbles:a,cancelable:!0});return o=st(o,n),c&&o.preventDefault(),r&&e.dispatchEvent(o),o.defaultPrevented&&s&&s.preventDefault(),o}};function st(e,t){for(const[n,s]of Object.entries(t||{}))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get(){return s}})}return e}const x=new Map,it={set(e,t,n){x.has(e)||x.set(e,new Map);const s=x.get(e);if(!s.has(t)&&s.size!==0){console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`);return}s.set(t,n)},get(e,t){return x.has(e)?x.get(e).get(t)||null:null},remove(e,t){if(!x.has(e))return;const n=x.get(e);n.delete(t),n.size===0&&x.delete(e)}};function _n(e){if(e==="true")return!0;if(e==="false")return!1;if(e===Number(e).toString())return Number(e);if(e===""||e==="null")return null;if(typeof e!="string")return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function ht(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const b={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${ht(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${ht(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const o of n){let s=o.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),t[s]=_n(e.dataset[o])}return t},getDataAttribute(e,t){return _n(e.getAttribute(`data-bs-${ht(t)}`))}};class te{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=g(t)?b.getDataAttribute(t,"config"):{};return{...this.constructor.Default,...typeof n=="object"?n:{},...g(t)?b.getDataAttributes(t):{},...typeof e=="object"?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const n of Object.keys(t)){const s=t[n],o=e[n],i=g(o)?"element":Xr(o);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const _r="5.2.3";class u extends te{constructor(e,t){if(super(),e=E(e),!e)return;this._element=e,this._config=this._getConfig(t),it.set(this._element,this.constructor.DATA_KEY,this)}dispose(){it.remove(this._element,this.constructor.DATA_KEY),e.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){Yn(e,t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return it.get(E(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,typeof t=="object"?t:null)}static get VERSION(){return _r}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const Se=(t,n="hide")=>{const o=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;e.on(document,o,`[data-bs-dismiss="${s}"]`,function(e){if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),w(this))return;const o=v(this)||this.closest(`.${s}`),i=t.getOrCreateInstance(o);i[n]()})},jr="alert",dr="bs.alert",vn=`.${dr}`,rr=`close${vn}`,Ja=`closed${vn}`,Qa="fade",Ga="show";class de extends u{static get NAME(){return jr}close(){const t=e.trigger(this._element,rr);if(t.defaultPrevented)return;this._element.classList.remove(Ga);const n=this._element.classList.contains(Qa);this._queueCallback(()=>this._destroyElement(),this._element,n)}_destroyElement(){this._element.remove(),e.trigger(this._element,Ja),this.dispose()}static jQueryInterface(e){return this.each(function(){const t=de.getOrCreateInstance(this);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)})}}Se(de,"close"),c(de);const Ka="button",$a="bs.button",Ia=`.${$a}`,Da=".data-api",ba="active",fn='[data-bs-toggle="button"]',va=`click${Ia}${Da}`;class fe extends u{static get NAME(){return Ka}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle(ba))}static jQueryInterface(e){return this.each(function(){const t=fe.getOrCreateInstance(this);e==="toggle"&&t[e]()})}}e.on(document,va,fn,e=>{e.preventDefault();const t=e.target.closest(fn),n=fe.getOrCreateInstance(t);n.toggle()}),c(fe);const t={find(e,t=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(t,e))},findOne(e,t=document.documentElement){return Element.prototype.querySelector.call(t,e)},children(e,t){return[].concat(...e.children).filter(e=>e.matches(t))},parents(e,t){const s=[];let n=e.parentNode.closest(t);for(;n;)s.push(n),n=n.parentNode.closest(t);return s},prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!w(e)&&H(e))}},ga="swipe",$=".bs.swipe",pa=`touchstart${$}`,ma=`touchmove${$}`,ua=`touchend${$}`,na=`pointerdown${$}`,Xi=`pointerup${$}`,$i="touch",Vi="pen",Pi="pointer-event",Li=40,Ni={endCallback:null,leftCallback:null,rightCallback:null},Di={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Re extends te{constructor(e,t){if(super(),this._element=e,!e||!Re.isSupported())return;this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents()}static get Default(){return Ni}static get DefaultType(){return Di}static get NAME(){return ga}dispose(){e.off(this._element,$)}_start(e){if(!this._supportPointerEvents){this._deltaX=e.touches[0].clientX;return}this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),y(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){const e=Math.abs(this._deltaX);if(e<=Li)return;const t=e/this._deltaX;if(this._deltaX=0,!t)return;y(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(e.on(this._element,na,e=>this._start(e)),e.on(this._element,Xi,e=>this._end(e)),this._element.classList.add(Pi)):(e.on(this._element,pa,e=>this._start(e)),e.on(this._element,ma,e=>this._move(e)),e.on(this._element,ua,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&(e.pointerType===Vi||e.pointerType===$i)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const is="carousel",Mi="bs.carousel",k=`.${Mi}`,Wt=".data-api",ki="ArrowLeft",yi="ArrowRight",ji=500,Z="next",K="prev",P="left",Ae="right",vi=`slide${k}`,Ve=`slid${k}`,di=`keydown${k}`,li=`mouseenter${k}`,oi=`mouseleave${k}`,ti=`dragstart${k}`,Zo=`load${k}${Wt}`,qo=`click${k}${Wt}`,Lt="carousel",ye="active",Wo="slide",Bo="carousel-item-end",Io="carousel-item-start",Po="carousel-item-next",Ro="carousel-item-prev",Mt=".active",Ct=".carousel-item",Lo=Mt+Ct,No=".carousel-item img",Do=".carousel-indicators",zo="[data-bs-slide], [data-bs-slide-to]",To='[data-bs-ride="carousel"]',bo={[ki]:Ae,[yi]:P},go={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},lo={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ie extends u{constructor(e,n){super(e,n),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=t.findOne(Do,this._element),this._addEventListeners(),this._config.ride===Lt&&this.cycle()}static get Default(){return go}static get DefaultType(){return lo}static get NAME(){return is}next(){this._slide(Z)}nextWhenVisible(){!document.hidden&&H(this._element)&&this.next()}prev(){this._slide(K)}pause(){this._isSliding&&es(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){if(!this._config.ride)return;if(this._isSliding){e.one(this._element,Ve,()=>this.cycle());return}this.cycle()}to(t){const n=this._getItems();if(t>n.length-1||t<0)return;if(this._isSliding){e.one(this._element,Ve,()=>this.to(t));return}const s=this._getItemIndex(this._getActive());if(s===t)return;const o=t>s?Z:K;this._slide(o,n[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&e.on(this._element,di,e=>this._keydown(e)),this._config.pause==="hover"&&(e.on(this._element,li,()=>this.pause()),e.on(this._element,oi,()=>this._maybeEnableCycle())),this._config.touch&&Re.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const n of t.find(No,this._element))e.on(n,ti,e=>e.preventDefault());const n=()=>{if(this._config.pause!=="hover")return;this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),ji+this._config.interval)},s={leftCallback:()=>this._slide(this._directionToOrder(P)),rightCallback:()=>this._slide(this._directionToOrder(Ae)),endCallback:n};this._swipeHelper=new Re(this._element,s)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=bo[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const s=t.findOne(Mt,this._indicatorsElement);s.classList.remove(ye),s.removeAttribute("aria-current");const n=t.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add(ye),n.setAttribute("aria-current","true"))}_updateInterval(){const e=this._activeElement||this._getActive();if(!e)return;const t=Number.parseInt(e.getAttribute("data-bs-interval"),10);this._config.interval=t||this._config.defaultInterval}_slide(t,n=null){if(this._isSliding)return;const o=this._getActive(),a=t===Z,s=n||Le(this._getItems(),o,a,this._config.wrap);if(s===o)return;const c=this._getItemIndex(s),l=n=>e.trigger(this._element,n,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(o),to:c}),d=l(vi);if(d.defaultPrevented)return;if(!o||!s)return;const u=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(c),this._activeElement=s;const i=a?Io:Bo,r=a?Po:Ro;s.classList.add(r),ne(s),o.classList.add(i),s.classList.add(i);const h=()=>{s.classList.remove(i,r),s.classList.add(ye),o.classList.remove(ye,r,i),this._isSliding=!1,l(Ve)};this._queueCallback(h,o,this._isAnimated()),u&&this.cycle()}_isAnimated(){return this._element.classList.contains(Wo)}_getActive(){return t.findOne(Lo,this._element)}_getItems(){return t.find(Ct,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return a()?e===P?K:Z:e===P?Z:K}_orderToDirection(e){return a()?e===K?P:Ae:e===K?Ae:P}static jQueryInterface(e){return this.each(function(){const t=ie.getOrCreateInstance(this,e);if(typeof e=="number"){t.to(e);return}if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}}e.on(document,qo,zo,function(e){const n=v(this);if(!n||!n.classList.contains(Lt))return;e.preventDefault();const t=ie.getOrCreateInstance(n),s=this.getAttribute("data-bs-slide-to");if(s){t.to(s),t._maybeEnableCycle();return}if(b.getDataAttribute(this,"slide")==="next"){t.next(),t._maybeEnableCycle();return}t.prev(),t._maybeEnableCycle()}),e.on(window,Zo,()=>{const e=t.find(To);for(const t of e)ie.getOrCreateInstance(t)}),c(ie);const os="collapse",ao="bs.collapse",ee=`.${ao}`,eo=".data-api",Zs=`show${ee}`,Xs=`shown${ee}`,Gs=`hide${ee}`,qs=`hidden${ee}`,Ks=`click${ee}${eo}`,rt="show",V="collapse",je="collapsing",Us="collapsed",Ws=`:scope .${V} .${V}`,$s="collapse-horizontal",Vs="width",Bs="height",Is=".collapse.show, .collapse.collapsing",et='[data-bs-toggle="collapse"]',Hs={parent:null,toggle:!0},Ps={parent:"(null|element)",toggle:"boolean"};class le extends u{constructor(e,n){super(e,n),this._isTransitioning=!1,this._triggerArray=[];const s=t.find(et);for(const e of s){const n=ns(e),o=t.find(n).filter(e=>e===this._element);n!==null&&o.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Hs}static get DefaultType(){return Ps}static get NAME(){return os}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let n=[];if(this._config.parent&&(n=this._getFirstLevelChildren(Is).filter(e=>e!==this._element).map(e=>le.getOrCreateInstance(e,{toggle:!1}))),n.length&&n[0]._isTransitioning)return;const s=e.trigger(this._element,Zs);if(s.defaultPrevented)return;for(const e of n)e.hide();const t=this._getDimension();this._element.classList.remove(V),this._element.classList.add(je),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const o=()=>{this._isTransitioning=!1,this._element.classList.remove(je),this._element.classList.add(V,rt),this._element.style[t]="",e.trigger(this._element,Xs)},i=t[0].toUpperCase()+t.slice(1),a=`scroll${i}`;this._queueCallback(o,this._element,!0),this._element.style[t]=`${this._element[a]}px`}hide(){if(this._isTransitioning||!this._isShown())return;const n=e.trigger(this._element,Gs);if(n.defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,ne(this._element),this._element.classList.add(je),this._element.classList.remove(V,rt);for(const e of this._triggerArray){const t=v(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0;const s=()=>{this._isTransitioning=!1,this._element.classList.remove(je),this._element.classList.add(V),e.trigger(this._element,qs)};this._element.style[t]="",this._queueCallback(s,this._element,!0)}_isShown(e=this._element){return e.classList.contains(rt)}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=E(e.parent),e}_getDimension(){return this._element.classList.contains($s)?Vs:Bs}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(et);for(const t of e){const n=v(t);n&&this._addAriaAndCollapsedClass([t],this._isShown(n))}}_getFirstLevelChildren(e){const n=t.find(Ws,this._config.parent);return t.find(e,this._config.parent).filter(e=>!n.includes(e))}_addAriaAndCollapsedClass(e,t){if(!e.length)return;for(const n of e)n.classList.toggle(Us,!t),n.setAttribute("aria-expanded",t)}static jQueryInterface(e){const t={};return typeof e=="string"&&/show|hide/.test(e)&&(t.toggle=!1),this.each(function(){const n=le.getOrCreateInstance(this,t);if(typeof e=="string"){if(typeof n[e]=="undefined")throw new TypeError(`No method named "${e}"`);n[e]()}})}}e.on(document,Ks,et,function(e){(e.target.tagName==="A"||e.delegateTarget&&e.delegateTarget.tagName==="A")&&e.preventDefault();const n=ns(this),s=t.find(n);for(const e of s)le.getOrCreateInstance(e,{toggle:!1}).toggle()}),c(le);var O,z,s="top",se,Ln,Bn,ae,Gn,Qn,Je,St,At,kt,Et,he,o="bottom",i="right",n="left",xe="auto",G=[s,o,i,n],F="start",W="end",$t="clippingParents",ze="viewport",L="popper",Kt="reference",Ie=G.reduce(function(e,t){return e.concat([t+"-"+F,t+"-"+W])},[]),Be=[].concat(G,[xe]).reduce(function(e,t){return e.concat([t,t+"-"+F,t+"-"+W])},[]),Gt="beforeRead",Xt="read",Qt="afterRead",Zt="beforeMain",Jt="main",en="afterMain",tn="beforeWrite",nn="write",sn="afterWrite",on=[Gt,Xt,Qt,Zt,Jt,en,tn,nn,sn];function f(e){return e?(e.nodeName||"").toLowerCase():null}function r(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t?t.defaultView||window:window}return e}function D(e){var t=r(e).Element;return e instanceof t||e instanceof Element}function l(e){var t=r(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}function Ye(e){if(typeof ShadowRoot=="undefined")return!1;var t=r(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}function Ss(e){var t=e.state;Object.keys(t.elements).forEach(function(e){var o=t.styles[e]||{},s=t.attributes[e]||{},n=t.elements[e];if(!l(n)||!f(n))return;Object.assign(n.style,o),Object.keys(s).forEach(function(e){var t=s[e];t===!1?n.removeAttribute(e):n.setAttribute(e,t===!0?"":t)})})}function Es(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(e){var s=t.elements[e],o=t.attributes[e]||{},i=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]),a=i.reduce(function(e,t){return e[t]="",e},{});if(!l(s)||!f(s))return;Object.assign(s.style,a),Object.keys(o).forEach(function(e){s.removeAttribute(e)})})}}const Ze={name:"applyStyles",enabled:!0,phase:"write",fn:Ss,effect:Es,requires:["computeStyles"]};function m(e){return e.split("-")[0]}O=Math.max,se=Math.min,z=Math.round;function nt(){var e=navigator.userAgentData;return e!=null&&e.brands?e.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function jn(){return!/^((?!chrome|android).)*safari/i.test(nt())}function X(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!1),s=e.getBoundingClientRect(),o=1,i=1,t&&l(e)&&(o=e.offsetWidth>0?z(s.width)/e.offsetWidth||1:1,i=e.offsetHeight>0?z(s.height)/e.offsetHeight||1:1);var s,o,i,f=D(e)?r(e):window,a=f.visualViewport,u=!jn()&&n,c=(s.left+(u&&a?a.offsetLeft:0))/o,d=(s.top+(u&&a?a.offsetTop:0))/i,h=s.width/o,m=s.height/i;return{width:h,height:m,top:d,right:c+h,bottom:d+m,left:c,x:c,y:d}}function dt(e){var t=X(e),n=e.offsetWidth,s=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-s)<=1&&(s=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:s}}function wn(e,t){var n,s=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(s&&Ye(s)){n=t;do{if(n&&e.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function p(e){return r(e).getComputedStyle(e)}function xs(e){return["table","td","th"].indexOf(f(e))>=0}function _(e){return((D(e)?e.ownerDocument:e.document)||window.document).documentElement}function ve(e){return f(e)==="html"?e:e.assignedSlot||e.parentNode||(Ye(e)?e.host:null)||_(e)}function kn(e){return!l(e)||p(e).position==="fixed"?null:e.offsetParent}function _s(e){var t,n,o,s=/firefox/i.test(nt()),i=/Trident/i.test(nt());if(i&&l(e)&&(o=p(e),o.position==="fixed"))return null;for(t=ve(e),Ye(t)&&(t=t.host);l(t)&&["html","body"].indexOf(f(t))<0;){if(n=p(t),n.transform!=="none"||n.perspective!=="none"||n.contain==="paint"||["transform","perspective"].indexOf(n.willChange)!==-1||s&&n.willChange==="filter"||s&&n.filter&&n.filter!=="none")return t;t=t.parentNode}return null}function re(e){for(var n=r(e),t=kn(e);t&&xs(t)&&p(t).position==="static";)t=kn(t);return t&&(f(t)==="html"||f(t)==="body"&&p(t).position==="static")?n:t||_s(e)||n}function Ue(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function oe(e,t,n){return O(e,se(t,n))}function js(e,t,n){var s=oe(e,t,n);return s>n?n:s}function zn(){return{top:0,right:0,bottom:0,left:0}}function Dn(e){return Object.assign({},zn(),e)}function Nn(e,t){return t.reduce(function(t,n){return t[n]=e,t},{})}Ln=function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,Dn(typeof t!="number"?t:Nn(t,G))};function bs(e){var r,c,d,u,p,g,v,b,j,y,_,O,x,C,E,t=e.state,S=e.name,A=e.options,h=t.elements.arrow,f=t.modifiersData.popperOffsets,w=m(t.placement),a=Ue(w),k=[n,i].indexOf(w)>=0,l=k?"height":"width";if(!h||!f)return;g=Ln(A.padding,t),v=dt(h),b=a==="y"?s:n,j=a==="y"?o:i,y=t.rects.reference[l]+t.rects.reference[a]-f[a]-t.rects.popper[l],_=f[a]-t.rects.reference[a],c=re(h),p=c?a==="y"?c.clientHeight||0:c.clientWidth||0:0,O=y/2-_/2,x=g[b],C=p-v[l]-g[j],u=p/2-v[l]/2+O,d=oe(x,u,C),E=a,t.modifiersData[S]=(r={},r[E]=d,r.centerOffset=d-u,r)}function vs(e){var n=e.state,o=e.options,s=o.element,t=s===void 0?"[data-popper-arrow]":s;if(t==null)return;if(typeof t=="string"&&(t=n.elements.popper.querySelector(t),!t))return;if(!wn(n.elements.popper,t))return;n.elements.arrow=t}const Hn={name:"arrow",enabled:!0,phase:"main",fn:bs,effect:vs,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Q(e){return e.split("-")[1]}Bn={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ms(e){var n=e.x,s=e.y,o=window,t=o.devicePixelRatio||1;return{x:z(n*t)/t||0,y:z(s*t)/t||0}}function $n(e){var c,u,h,b,j,y,x,T,z,f=e.popper,D=e.popperRect,d=e.placement,k=e.variation,m=e.offsets,S=e.position,v=e.gpuAcceleration,A=e.adaptive,w=e.roundOffsets,L=e.isFixed,N=m.x,t=N===void 0?0:N,M=m.y,a=M===void 0?0:M,E=typeof w=="function"?w({x:t,y:a}):{x:t,y:a},t=E.x,a=E.y,F=m.hasOwnProperty("x"),C=m.hasOwnProperty("y"),O=n,g=s,l=window;return A&&(c=re(f),y="clientHeight",j="clientWidth",c===r(f)&&(c=_(f),p(c).position!=="static"&&S==="absolute"&&(y="scrollHeight",j="scrollWidth")),c=c,(d===s||(d===n||d===i)&&k===W)&&(g=o,T=L&&c===l&&l.visualViewport?l.visualViewport.height:c[y],a-=T-D.height,a*=v?1:-1),(d===n||(d===s||d===o)&&k===W)&&(O=i,z=L&&c===l&&l.visualViewport?l.visualViewport.width:c[j],t-=z-D.width,t*=v?1:-1)),x=Object.assign({position:S},A&&Bn),b=w===!0?ms({x:t,y:a}):{x:t,y:a},t=b.x,a=b.y,v?Object.assign({},x,(h={},h[g]=C?"0":"",h[O]=F?"0":"",h.transform=(l.devicePixelRatio||1)<=1?"translate("+t+"px, "+a+"px)":"translate3d("+t+"px, "+a+"px, 0)",h)):Object.assign({},x,(u={},u[g]=C?a+"px":"",u[O]=F?t+"px":"",u.transform="",u))}function hs(e){var t=e.state,n=e.options,s=n.gpuAcceleration,c=s===void 0||s,o=n.adaptive,l=o===void 0||o,i=n.roundOffsets,a=i===void 0||i,r={placement:m(t.placement),variation:Q(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:c,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,$n(Object.assign({},r,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:l,roundOffsets:a})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,$n(Object.assign({},r,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:a})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}const Te={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:hs,data:{}};ae={passive:!0};function ls(e){var n=e.state,t=e.instance,s=e.options,o=s.scroll,i=o===void 0||o,a=s.resize,c=a===void 0||a,l=r(n.elements.popper),d=[].concat(n.scrollParents.reference,n.scrollParents.popper);return i&&d.forEach(function(e){e.addEventListener("scroll",t.update,ae)}),c&&l.addEventListener("resize",t.update,ae),function(){i&&d.forEach(function(e){e.removeEventListener("scroll",t.update,ae)}),c&&l.removeEventListener("resize",t.update,ae)}}const Pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:ls,data:{}};Gn={left:"right",right:"left",bottom:"top",top:"bottom"};function ke(e){return e.replace(/left|right|bottom|top/g,function(e){return Gn[e]})}Qn={start:"end",end:"start"};function Zn(e){return e.replace(/start|end/g,function(e){return Qn[e]})}function Ke(e){var t=r(e),n=t.pageXOffset,s=t.pageYOffset;return{scrollLeft:n,scrollTop:s}}function qe(e){return X(_(e)).left+Ke(e).scrollLeft}function rs(e,t){var s,d=r(e),o=_(e),n=d.visualViewport,i=o.clientWidth,a=o.clientHeight,c=0,l=0;return n&&(i=n.width,a=n.height,s=jn(),(s||!s&&t==="fixed")&&(c=n.offsetLeft,l=n.offsetTop)),{width:i,height:a,x:c+qe(e),y:l}}function Fi(e){var s,n=_(e),o=Ke(e),t=(s=e.ownerDocument)==null?void 0:s.body,i=O(n.scrollWidth,n.clientWidth,t?t.scrollWidth:0,t?t.clientWidth:0),r=O(n.scrollHeight,n.clientHeight,t?t.scrollHeight:0,t?t.clientHeight:0),a=-o.scrollLeft+qe(e),c=-o.scrollTop;return p(t||n).direction==="rtl"&&(a+=O(n.clientWidth,t?t.clientWidth:0)-i),{width:i,height:r,x:a,y:c}}function at(e){var t=p(e),n=t.overflow,s=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+s)}function zt(e){return["html","body","#document"].indexOf(f(e))>=0?e.ownerDocument.body:l(e)&&at(e)?e:zt(ve(e))}function ce(e,t){t===void 0&&(t=[]);var s,n=zt(e),o=n===((s=e.ownerDocument)==null?void 0:s.body),i=r(n),a=o?[i].concat(i.visualViewport||[],at(n)?n:[]):n,c=t.concat(a);return o?c:c.concat(ce(ve(a)))}function Fe(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function cs(e,t){var n=X(e,!1,t==="fixed");return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}function qn(e,t,n){return t===ze?Fe(rs(e,n)):D(t)?cs(t,n):Fe(Fi(_(e)))}function ds(e){var n=ce(ve(e)),s=["absolute","fixed"].indexOf(p(e).position)>=0,t=s&&l(e)?re(e):e;return D(t)?n.filter(function(e){return D(e)&&wn(e,t)&&f(e)!=="body"}):[]}function us(e,t,n,s){var a=t==="clippingParents"?ds(e):[].concat(t),i=[].concat(a,[n]),r=i[0],o=i.reduce(function(t,n){var o=qn(e,n,s);return t.top=O(o.top,t.top),t.right=se(o.right,t.right),t.bottom=se(o.bottom,t.bottom),t.left=O(o.left,t.left),t},qn(e,r,s));return o.width=o.right-o.left,o.height=o.bottom-o.top,o.x=o.left,o.y=o.top,o}function Wn(e){var a,r,l,t=e.reference,c=e.element,d=e.placement,u=d?m(d):null,p=d?Q(d):null,h=t.x+t.width/2-c.width/2,f=t.y+t.height/2-c.height/2;switch(u){case s:a={x:h,y:t.y-c.height};break;case o:a={x:h,y:t.y+t.height};break;case i:a={x:t.x+t.width,y:f};break;case n:a={x:t.x-c.width,y:f};break;default:a={x:t.x,y:t.y}}if(r=u?Ue(u):null,r!=null)switch(l=r==="y"?"height":"width",p){case F:a[r]=a[r]-(t[l]/2-c[l]/2);break;case W:a[r]=a[r]+(t[l]/2-c[l]/2);break}return a}function I(e,t){t===void 0&&(t={});var w,n=t,v=n.placement,j=v===void 0?e.placement:v,f=n.strategy,T=f===void 0?e.strategy:f,p=n.boundary,E=p===void 0?$t:p,x=n.rootBoundary,F=x===void 0?ze:x,C=n.elementContext,c=C===void 0?L:C,m=n.altBoundary,M=m!==void 0&&m,b=n.padding,d=b===void 0?0:b,a=Dn(typeof d!="number"?d:Nn(d,G)),S=c===L?Kt:L,O=e.rects.popper,h=e.elements[M?S:c],r=us(D(h)?h:h.contextElement||_(e.elements.popper),E,F,T),y=X(e.elements.reference),k=Wn({reference:y,element:O,strategy:"absolute",placement:j}),A=Fe(Object.assign({},O,k)),l=c===L?A:y,u={top:r.top-l.top+a.top,bottom:l.bottom-r.bottom+a.bottom,left:r.left-l.left+a.left,right:l.right-r.right+a.right},g=e.modifiersData.offset;return c===L&&g&&(w=g[j],Object.keys(u).forEach(function(e){var t=[i,o].indexOf(e)>=0?1:-1,n=[s,o].indexOf(e)>=0?"y":"x";u[e]+=w[n]*t})),u}function fs(e,t){t===void 0&&(t={});var s,n=t,c=n.placement,l=n.boundary,d=n.rootBoundary,u=n.padding,h=n.flipVariations,i=n.allowedAutoPlacements,f=i===void 0?Be:i,a=Q(c),r=a?h?Ie:Ie.filter(function(e){return Q(e)===a}):G,o=r.filter(function(e){return f.indexOf(e)>=0});return o.length===0&&(o=r),s=o.reduce(function(t,n){return t[n]=I(e,{placement:n,boundary:l,rootBoundary:d,padding:u})[m(n)],t},{}),Object.keys(s).sort(function(e,t){return s[e]-s[t]})}function ps(e){if(m(e)===xe)return[];var t=ke(e);return[Zn(e),t,Zn(t)]}function gs(e){var r,c,l,u,h,g,v,y,_,x,k,t=e.state,a=e.options,C=e.name;if(t.modifiersData[C]._skip)return;for(var E=a.mainAxis,H=E===void 0||E,M=a.altAxis,R=M===void 0||M,L=a.fallbackPlacements,z=a.padding,w=a.boundary,O=a.rootBoundary,N=a.altBoundary,T=a.flipVariations,j=T===void 0||T,P=a.allowedAutoPlacements,d=t.options.placement,U=m(d),D=U===d,K=L||(D||!j?[ke(d)]:ps(d)),p=[d].concat(K).reduce(function(e,n){return e.concat(m(n)===xe?fs(t,{placement:n,boundary:w,rootBoundary:O,padding:z,flipVariations:j,allowedAutoPlacements:P}):n)},[]),V=t.rects.reference,B=t.rects.popper,A=new Map,S=!0,f=p[0],b=0;b=0,_=y?"width":"height",h=I(t,{placement:r,boundary:w,rootBoundary:O,altBoundary:N,padding:z}),l=y?g?i:n:g?o:s,V[_]>B[_]&&(l=ke(l)),k=ke(l),c=[],H&&c.push(h[v]<=0),R&&c.push(h[l]<=0,h[k]<=0),c.every(function(e){return e})){f=r,S=!1;break}A.set(r,c)}if(S)for($=j?3:1,W=function(t){var n=p.find(function(e){var n=A.get(e);if(n)return n.slice(0,t).every(function(e){return e})});if(n)return f=n,"break"},u=$;u>0;u--)if(x=W(u),x==="break")break;t.placement!==f&&(t.modifiersData[C]._skip=!0,t.placement=f,t.reset=!0)}const Pn={name:"flip",enabled:!0,phase:"main",fn:gs,requiresIfExists:["offset"],data:{_skip:!1}};function Rn(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function Tn(e){return[s,i,o,n].some(function(t){return e[t]>=0})}function ys(e){var t=e.state,a=e.name,r=t.rects.reference,c=t.rects.popper,l=t.modifiersData.preventOverflow,d=I(t,{elementContext:"reference"}),u=I(t,{altBoundary:!0}),n=Rn(d,r),s=Rn(u,c,l),o=Tn(n),i=Tn(s);t.modifiersData[a]={referenceClippingOffsets:n,popperEscapeOffsets:s,isReferenceHidden:o,hasPopperEscaped:i},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":o,"data-popper-escaped":i})}const An={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:ys};function ws(e,t,o){var c=m(e),d=[n,s].indexOf(c)>=0?-1:1,l=typeof o=="function"?o(Object.assign({},t,{placement:e})):o,a=l[0],r=l[1],a=a||0,r=(r||0)*d;return[n,i].indexOf(c)>=0?{x:r,y:a}:{x:a,y:r}}function Os(e){var t=e.state,i=e.options,a=e.name,n=i.offset,r=n===void 0?[0,0]:n,s=Be.reduce(function(e,n){return e[n]=ws(n,t.rects,r),e},{}),o=s[t.placement],c=o.x,l=o.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=c,t.modifiersData.popperOffsets.y+=l),t.modifiersData[a]=s}const xn={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Os};function Cs(e){var t=e.state,n=e.name;t.modifiersData[n]=Wn({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}const Qe={name:"popperOffsets",enabled:!0,phase:"read",fn:Cs,data:{}};function ks(e){return e==="x"?"y":"x"}function As(e){var r,c,h,p,v,w,C,k,A,M,T,z,D,L,R,P,H,B,V,$,W,U,K,q,Y,G,X,Z,t=e.state,l=e.options,be=e.name,fe,ue,ee,te,ie,ce,le,me,pe=l.mainAxis,ge=pe===void 0||pe,ne=l.altAxis,we=ne!==void 0&&ne,_e=l.boundary,ye=l.rootBoundary,ve=l.altBoundary,je=l.padding,de=l.tether,d=de===void 0||de,ae=l.tetherOffset,S=ae===void 0?0:ae,x=I(t,{boundary:_e,rootBoundary:ye,padding:je,altBoundary:ve}),J=m(t.placement),E=Q(t.placement),he=!E,a=Ue(J),j=ks(a),b=t.modifiersData.popperOffsets,u=t.rects.reference,g=t.rects.popper,_=typeof S=="function"?S(Object.assign({},t.rects,{placement:t.placement})):S,f=typeof _=="number"?{mainAxis:_,altAxis:_}:Object.assign({mainAxis:0,altAxis:0},_),y=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,N={x:0,y:0};if(!b)return;ge&&(R=a==="y"?s:n,P=a==="y"?o:i,r=a==="y"?"height":"width",h=b[a],V=h+x[R],$=h-x[P],W=d?-g[r]/2:0,Z=E===F?u[r]:g[r],X=E===F?-g[r]:-u[r],q=t.elements.arrow,ue=d&&q?dt(q):{width:0,height:0},k=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:zn(),K=k[R],U=k[P],v=oe(0,u[r],ue[r]),ee=he?u[r]/2-W-v-K-f.mainAxis:Z-v-K-f.mainAxis,te=he?-u[r]/2+W+v+U+f.mainAxis:X+v+U+f.mainAxis,C=t.elements.arrow&&re(t.elements.arrow),ie=C?a==="y"?C.clientTop||0:C.clientLeft||0:0,B=(fe=y?.[a])!=null?fe:0,ce=h+ee-B-ie,le=h+te-B,H=oe(d?se(V,ce):V,h,d?O($,le):$),b[a]=H,N[a]=H-h),we&&(Y=a==="x"?s:n,me=a==="x"?o:i,c=b[j],p=j==="y"?"height":"width",L=c+x[Y],D=c-x[me],w=[s,n].indexOf(J)!==-1,z=(G=y?.[j])!=null?G:0,T=w?L:c-u[p]-g[p]-z+f.altAxis,M=w?c+u[p]+g[p]-z-f.altAxis:D,A=d&&w?js(T,c,M):oe(d?T:L,c,d?M:D),b[j]=A,N[j]=A-c),t.modifiersData[be]=N}const un={name:"preventOverflow",enabled:!0,phase:"main",fn:As,requiresIfExists:["offset"]};function Ms(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}function Fs(e){return e===r(e)||!l(e)?Ke(e):Ms(e)}function Ts(e){var t=e.getBoundingClientRect(),n=z(t.width)/e.offsetWidth||1,s=z(t.height)/e.offsetHeight||1;return n!==1||s!==1}function zs(e,t,n){n===void 0&&(n=!1);var r=l(t),c=l(t)&&Ts(t),i=_(t),o=X(e,c,n),a={scrollLeft:0,scrollTop:0},s={x:0,y:0};return(r||!r&&!n)&&((f(t)!=="body"||at(i))&&(a=Fs(t)),l(t)?(s=X(t,!0),s.x+=t.clientLeft,s.y+=t.clientTop):i&&(s.x=qe(i))),{x:o.left+a.scrollLeft-s.x,y:o.top+a.scrollTop-s.y,width:o.width,height:o.height}}function Ds(e){var n=new Map,t=new Set,s=[];e.forEach(function(e){n.set(e.name,e)});function o(e){t.add(e.name);var i=[].concat(e.requires||[],e.requiresIfExists||[]);i.forEach(function(e){if(!t.has(e)){var s=n.get(e);s&&o(s)}}),s.push(e)}return e.forEach(function(e){t.has(e.name)||o(e)}),s}function Ns(e){var t=Ds(e);return on.reduce(function(e,n){return e.concat(t.filter(function(e){return e.phase===n}))},[])}function Ls(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}function Rs(e){var t=e.reduce(function(e,t){var n=e[t.name];return e[t.name]=n?Object.assign({},n,t,{options:Object.assign({},n.options,t.options),data:Object.assign({},n.data,t.data)}):t,e},{});return Object.keys(t).map(function(e){return t[e]})}Je={placement:"bottom",modifiers:[],strategy:"absolute"};function Tt(){for(var t=arguments.length,n=new Array(t),e=0;eNumber.parseInt(e,10)):typeof e=="function"?t=>e(t,this._element):e}_getPopperConfig(){const e={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||this._config.display==="static")&&(b.setDataAttribute(this._menu,"popper","static"),e.modifiers=[{name:"applyStyles",enabled:!1}]),{...e,...typeof this._config.popperConfig=="function"?this._config.popperConfig(e):this._config.popperConfig}}_selectMenuItem({key:e,target:n}){const s=t.find(_o,this._menu).filter(e=>H(e));if(!s.length)return;Le(s,n,e===gt,!s.includes(n)).focus()}static jQueryInterface(e){return this.each(function(){const t=h.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}static clearMenus(e){if(e.button===to||e.type==="keyup"&&e.key!==vt)return;const n=t.find(vo);for(const a of n){const t=h.getInstance(a);if(!t||t._config.autoClose===!1)continue;const s=e.composedPath(),o=s.includes(t._menu);if(s.includes(t._element)||t._config.autoClose==="inside"&&!o||t._config.autoClose==="outside"&&o)continue;if(t._menu.contains(e.target)&&(e.type==="keyup"&&e.key===vt||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;const i={relatedTarget:t._element};e.type==="click"&&(i.clickEvent=e),t._completeHide(i)}}static dataApiKeydownHandler(e){const a=/input|textarea/i.test(e.target.tagName),s=e.key===Qs,o=[Js,gt].includes(e.key);if(!o&&!s)return;if(a&&!s)return;e.preventDefault();const i=this.matches(S)?this:t.prev(this,S)[0]||t.next(this,S)[0]||t.findOne(S,e.delegateTarget.parentNode),n=h.getOrCreateInstance(i);if(o){e.stopPropagation(),n.show(),n._selectMenuItem(e);return}n._isShown()&&(e.stopPropagation(),n.hide(),i.focus())}}e.on(document,jt,S,h.dataApiKeydownHandler),e.on(document,jt,be,h.dataApiKeydownHandler),e.on(document,bt,h.clearMenus),e.on(document,co,h.clearMenus),e.on(document,bt,S,function(e){e.preventDefault(),h.getOrCreateInstance(this).toggle()}),c(h);const wt=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ot=".sticky-top",me="padding-right",xt="margin-right";class tt{constructor(){this._element=document.body}getWidth(){const e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){const e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,me,t=>t+e),this._setElementAttributes(wt,me,t=>t+e),this._setElementAttributes(Ot,xt,t=>t-e)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,me),this._resetElementAttributes(wt,me),this._resetElementAttributes(Ot,xt)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(e,t,n){const s=this.getWidth(),o=e=>{if(e!==this._element&&window.innerWidth>e.clientWidth+s)return;this._saveInitialAttribute(e,t);const o=window.getComputedStyle(e).getPropertyValue(t);e.style.setProperty(t,`${n(Number.parseFloat(o))}px`)};this._applyManipulationCallback(e,o)}_saveInitialAttribute(e,t){const n=e.style.getPropertyValue(t);n&&b.setDataAttribute(e,t,n)}_resetElementAttributes(e,t){const n=e=>{const n=b.getDataAttribute(e,t);if(n===null){e.style.removeProperty(t);return}b.removeDataAttribute(e,t),e.style.setProperty(t,n)};this._applyManipulationCallback(e,n)}_applyManipulationCallback(e,n){if(g(e)){n(e);return}for(const s of t.find(e,this._element))n(s)}}const Ft="backdrop",Ho="fade",pt="show",Dt=`mousedown.bs.${Ft}`,Vo={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},$o={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Nt extends te{constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return Vo}static get DefaultType(){return $o}static get NAME(){return Ft}show(e){if(!this._config.isVisible){y(e);return}this._append();const t=this._getElement();this._config.isAnimated&&ne(t),t.classList.add(pt),this._emulateAnimation(()=>{y(e)})}hide(e){if(!this._config.isVisible){y(e);return}this._getElement().classList.remove(pt),this._emulateAnimation(()=>{this.dispose(),y(e)})}dispose(){if(!this._isAppended)return;e.off(this._element,Dt),this._element.remove(),this._isAppended=!1}_getElement(){if(!this._element){const e=document.createElement("div");e.className=this._config.className,this._config.isAnimated&&e.classList.add(Ho),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=E(e.rootElement),e}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),e.on(t,Dt,()=>{y(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(e){Yn(e,this._getElement(),this._config.isAnimated)}}const Uo="focustrap",Ko="bs.focustrap",_e=`.${Ko}`,Yo=`focusin${_e}`,Go=`keydown.tab${_e}`,Xo="Tab",Qo="forward",Rt="backward",Jo={autofocus:!0,trapElement:null},ei={autofocus:"boolean",trapElement:"element"};class Pt extends te{constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Jo}static get DefaultType(){return ei}static get NAME(){return Uo}activate(){if(this._isActive)return;this._config.autofocus&&this._config.trapElement.focus(),e.off(document,_e),e.on(document,Yo,e=>this._handleFocusin(e)),e.on(document,Go,e=>this._handleKeydown(e)),this._isActive=!0}deactivate(){if(!this._isActive)return;this._isActive=!1,e.off(document,_e)}_handleFocusin(e){const{trapElement:n}=this._config;if(e.target===document||e.target===n||n.contains(e.target))return;const s=t.focusableChildren(n);s.length===0?n.focus():this._lastTabNavDirection===Rt?s[s.length-1].focus():s[0].focus()}_handleKeydown(e){if(e.key!==Xo)return;this._lastTabNavDirection=e.shiftKey?Rt:Qo}}const ni="modal",si="bs.modal",d=`.${si}`,ii=".data-api",ai="Escape",ri=`hide${d}`,ci=`hidePrevented${d}`,Ht=`hidden${d}`,It=`show${d}`,ui=`shown${d}`,hi=`resize${d}`,mi=`click.dismiss${d}`,fi=`mousedown.dismiss${d}`,pi=`keydown.dismiss${d}`,gi=`click${d}${ii}`,Bt="modal-open",bi="fade",Vt="show",Ne="modal-static",_i=".modal.show",wi=".modal-dialog",Oi=".modal-body",xi='[data-bs-toggle="modal"]',Ci={backdrop:!0,focus:!0,keyboard:!0},Ei={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class B extends u{constructor(e,n){super(e,n),this._dialog=t.findOne(wi,this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new tt,this._addEventListeners()}static get Default(){return Ci}static get DefaultType(){return Ei}static get NAME(){return ni}toggle(e){return this._isShown?this.hide():this.show(e)}show(t){if(this._isShown||this._isTransitioning)return;const n=e.trigger(this._element,It,{relatedTarget:t});if(n.defaultPrevented)return;this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Bt),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t))}hide(){if(!this._isShown||this._isTransitioning)return;const t=e.trigger(this._element,ri);if(t.defaultPrevented)return;this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Vt),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())}dispose(){for(const t of[window,this._dialog])e.off(t,d);this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Nt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Pt({trapElement:this._element})}_showElement(n){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const s=t.findOne(Oi,this._dialog);s&&(s.scrollTop=0),ne(this._element),this._element.classList.add(Vt);const o=()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,e.trigger(this._element,ui,{relatedTarget:n})};this._queueCallback(o,this._dialog,this._isAnimated())}_addEventListeners(){e.on(this._element,pi,e=>{if(e.key!==ai)return;if(this._config.keyboard){e.preventDefault(),this.hide();return}this._triggerBackdropTransition()}),e.on(window,hi,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),e.on(this._element,fi,t=>{e.one(this._element,mi,e=>{if(this._element!==t.target||this._element!==e.target)return;if(this._config.backdrop==="static"){this._triggerBackdropTransition();return}this._config.backdrop&&this.hide()})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Bt),this._resetAdjustments(),this._scrollBar.reset(),e.trigger(this._element,Ht)})}_isAnimated(){return this._element.classList.contains(bi)}_triggerBackdropTransition(){const n=e.trigger(this._element,ci);if(n.defaultPrevented)return;const s=this._element.scrollHeight>document.documentElement.clientHeight,t=this._element.style.overflowY;if(t==="hidden"||this._element.classList.contains(Ne))return;s||(this._element.style.overflowY="hidden"),this._element.classList.add(Ne),this._queueCallback(()=>{this._element.classList.remove(Ne),this._queueCallback(()=>{this._element.style.overflowY=t},this._dialog)},this._dialog),this._element.focus()}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),n=e>0;if(n&&!t){const t=a()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!n&&t){const t=a()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(e,t){return this.each(function(){const n=B.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof n[e]=="undefined")throw new TypeError(`No method named "${e}"`);n[e](t)})}}e.on(document,gi,xi,function(n){const s=v(this);["A","AREA"].includes(this.tagName)&&n.preventDefault(),e.one(s,It,t=>{if(t.defaultPrevented)return;e.one(s,Ht,()=>{H(this)&&this.focus()})});const o=t.findOne(_i);o&&B.getInstance(o).hide();const i=B.getOrCreateInstance(s);i.toggle(this)}),Se(B),c(B);const Ai="offcanvas",Si="bs.offcanvas",j=`.${Si}`,Ut=".data-api",Ti=`load${j}${Ut}`,zi="Escape",qt="show",Yt="showing",an="hiding",Ri="offcanvas-backdrop",rn=".offcanvas.show",Hi=`show${j}`,Ii=`shown${j}`,Bi=`hide${j}`,cn=`hidePrevented${j}`,ln=`hidden${j}`,Wi=`resize${j}`,Ui=`click${j}${Ut}`,Ki=`keydown.dismiss${j}`,qi='[data-bs-toggle="offcanvas"]',Yi={backdrop:!0,keyboard:!0,scroll:!1},Gi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class A extends u{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Yi}static get DefaultType(){return Gi}static get NAME(){return Ai}toggle(e){return this._isShown?this.hide():this.show(e)}show(t){if(this._isShown)return;const n=e.trigger(this._element,Hi,{relatedTarget:t});if(n.defaultPrevented)return;this._isShown=!0,this._backdrop.show(),this._config.scroll||(new tt).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Yt);const s=()=>{(!this._config.scroll||this._config.backdrop)&&this._focustrap.activate(),this._element.classList.add(qt),this._element.classList.remove(Yt),e.trigger(this._element,Ii,{relatedTarget:t})};this._queueCallback(s,this._element,!0)}hide(){if(!this._isShown)return;const t=e.trigger(this._element,Bi);if(t.defaultPrevented)return;this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(an),this._backdrop.hide();const n=()=>{this._element.classList.remove(qt,an),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new tt).reset(),e.trigger(this._element,ln)};this._queueCallback(n,this._element,!0)}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const n=()=>{if(this._config.backdrop==="static"){e.trigger(this._element,cn);return}this.hide()},t=Boolean(this._config.backdrop);return new Nt({className:Ri,isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?n:null})}_initializeFocusTrap(){return new Pt({trapElement:this._element})}_addEventListeners(){e.on(this._element,Ki,t=>{if(t.key!==zi)return;if(!this._config.keyboard){e.trigger(this._element,cn);return}this.hide()})}static jQueryInterface(e){return this.each(function(){const t=A.getOrCreateInstance(this,e);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)})}}e.on(document,Ui,qi,function(n){const s=v(this);if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),w(this))return;e.one(s,ln,()=>{H(this)&&this.focus()});const o=t.findOne(rn);o&&o!==s&&A.getInstance(o).hide();const i=A.getOrCreateInstance(s);i.toggle(this)}),e.on(window,Ti,()=>{for(const e of t.find(rn))A.getOrCreateInstance(e).show()}),e.on(window,Wi,()=>{for(const e of t.find("[aria-modal][class*=show][class*=offcanvas-]"))getComputedStyle(e).position!=="fixed"&&A.getOrCreateInstance(e).hide()}),Se(A),c(A);const Qi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Zi=/^aria-[\w-]*$/i,Ji=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,ea=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,ta=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!Qi.has(n)||Boolean(Ji.test(e.nodeValue)||ea.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))},dn={"*":["class","dir","id","lang","role",Zi],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]};function sa(e,t,n){if(!e.length)return e;if(n&&typeof n=="function")return n(e);const o=new window.DOMParser,s=o.parseFromString(e,"text/html"),i=[].concat(...s.body.querySelectorAll("*"));for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[].concat(...e.attributes),o=[].concat(t["*"]||[],t[n]||[]);for(const t of s)ta(t,o)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const oa="TemplateFactory",ia={allowList:dn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
      "},aa={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ra={entry:"(string|element|function|null)",selector:"(string|element)"};class ca extends te{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return ia}static get DefaultType(){return aa}static get NAME(){return oa}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},ra)}_setContent(e,n,s){const o=t.findOne(s,e);if(!o)return;if(n=this._resolvePossibleFunction(n),!n){o.remove();return}if(g(n)){this._putElementInTemplate(E(n),o);return}if(this._config.html){o.innerHTML=this._maybeSanitize(n);return}o.textContent=n}_maybeSanitize(e){return this._config.sanitize?sa(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return typeof e=="function"?e(this):e}_putElementInTemplate(e,t){if(this._config.html){t.innerHTML="",t.append(e);return}t.textContent=e.textContent}}const la="tooltip",da=new Set(["sanitize","allowList","sanitizeFn"]),Xe="fade",ha="modal",ue="show",fa=".tooltip-inner",hn=`.${ha}`,mn="hide.bs.modal",J="hover",ot="focus",ja="click",ya="manual",_a="hide",wa="hidden",Oa="show",xa="shown",Ca="inserted",Ea="click",ka="focusin",Aa="focusout",Sa="mouseenter",Ma="mouseleave",Fa={AUTO:"auto",TOP:"top",RIGHT:a()?"left":"right",BOTTOM:"bottom",LEFT:a()?"right":"left"},Ta={allowList:dn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},za={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class U extends u{constructor(e,t){if(typeof _t=="undefined")throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Ta}static get DefaultType(){return za}static get NAME(){return la}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){if(!this._isEnabled)return;if(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()){this._leave();return}this._enter()}dispose(){clearTimeout(this._timeout),e.off(this._element.closest(hn),mn,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if(this._element.style.display==="none")throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const n=e.trigger(this._element,this.constructor.eventName(Oa)),s=Jn(this._element),o=(s||this._element.ownerDocument.documentElement).contains(this._element);if(n.defaultPrevented||!o)return;this._disposePopper();const t=this._getTipElement();this._element.setAttribute("aria-describedby",t.getAttribute("id"));const{container:i}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(i.append(t),e.trigger(this._element,this.constructor.eventName(Ca))),this._popper=this._createPopper(t),t.classList.add(ue),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))e.on(t,"mouseover",Oe);const a=()=>{e.trigger(this._element,this.constructor.eventName(xa)),this._isHovered===!1&&this._leave(),this._isHovered=!1};this._queueCallback(a,this.tip,this._isAnimated())}hide(){if(!this._isShown())return;const t=e.trigger(this._element,this.constructor.eventName(_a));if(t.defaultPrevented)return;const n=this._getTipElement();if(n.classList.remove(ue),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))e.off(t,"mouseover",Oe);this._activeTrigger[ja]=!1,this._activeTrigger[ot]=!1,this._activeTrigger[J]=!1,this._isHovered=null;const s=()=>{if(this._isWithActiveTrigger())return;this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),e.trigger(this._element,this.constructor.eventName(wa))};this._queueCallback(s,this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(Xe,ue),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=Gr(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add(Xe),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new ca({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[fa]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Xe)}_isShown(){return this.tip&&this.tip.classList.contains(ue)}_createPopper(e){const t=typeof this._config.placement=="function"?this._config.placement.call(this,e,this._element):this._config.placement,n=Fa[t.toUpperCase()];return he(this._element,e,this._getPopperConfig(n))}_getOffset(){const{offset:e}=this._config;return typeof e=="string"?e.split(",").map(e=>Number.parseInt(e,10)):typeof e=="function"?t=>e(t,this._element):e}_resolvePossibleFunction(e){return typeof e=="function"?e.call(this._element):e}_getPopperConfig(e){const t={placement:e,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:e=>{this._getTipElement().setAttribute("data-popper-placement",e.state.placement)}}]};return{...t,...typeof this._config.popperConfig=="function"?this._config.popperConfig(t):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const n of t)if(n==="click")e.on(this._element,this.constructor.eventName(Ea),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t.toggle()});else if(n!==ya){const t=n===J?this.constructor.eventName(Sa):this.constructor.eventName(ka),s=n===J?this.constructor.eventName(Ma):this.constructor.eventName(Aa);e.on(this._element,t,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[e.type==="focusin"?ot:J]=!0,t._enter()}),e.on(this._element,s,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[e.type==="focusout"?ot:J]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},e.on(this._element.closest(hn),mn,this._hideModalHandler)}_fixTitle(){const e=this._element.getAttribute("title");if(!e)return;!this._element.getAttribute("aria-label")&&!this._element.textContent.trim()&&this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title")}_enter(){if(this._isShown()||this._isHovered){this._isHovered=!0;return}this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show)}_leave(){if(this._isWithActiveTrigger())return;this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide)}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=b.getDataAttributes(this._element);for(const e of Object.keys(t))da.has(e)&&delete t[e];return e={...t,...typeof e=="object"&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=e.container===!1?document.body:E(e.container),typeof e.delay=="number"&&(e.delay={show:e.delay,hide:e.delay}),typeof e.title=="number"&&(e.title=e.title.toString()),typeof e.content=="number"&&(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const t in this._config)this.constructor.Default[t]!==this._config[t]&&(e[t]=this._config[t]);return e.selector=!1,e.trigger="manual",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each(function(){const t=U.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}}c(U);const Na="popover",La=".popover-header",Ra=".popover-body",Pa={...U.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Ha={...U.DefaultType,content:"(null|string|element|function)"};class ct extends U{static get Default(){return Pa}static get DefaultType(){return Ha}static get NAME(){return Na}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[La]:this._getTitle(),[Ra]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each(function(){const t=ct.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}}c(ct);const Ba="scrollspy",Va="bs.scrollspy",lt=`.${Va}`,Wa=".data-api",Ua=`activate${lt}`,pn=`click${lt}`,qa=`load${lt}${Wa}`,Ya="dropdown-item",q="active",Xa='[data-bs-spy="scroll"]',mt="[href]",Za=".nav, .list-group",gn=".nav-link",er=".nav-item",tr=".list-group-item",nr=`${gn}, ${er} > ${gn}, ${tr}`,sr=".dropdown",or=".dropdown-toggle",ir={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ar={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ee extends u{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=getComputedStyle(this._element).overflowY==="visible"?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ir}static get DefaultType(){return ar}static get NAME(){return Ba}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=E(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,typeof e.threshold=="string"&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_maybeEnableSmoothScroll(){if(!this._config.smoothScroll)return;e.off(this._config.target,pn),e.on(this._config.target,pn,mt,e=>{const t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();const n=this._rootElement||window,s=t.offsetTop-this._element.offsetTop;if(n.scrollTo){n.scrollTo({top:s,behavior:"smooth"});return}n.scrollTop=s}})}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(e=>this._observerCallback(e),e)}_observerCallback(e){const n=e=>this._targetLinks.get(`#${e.target.id}`),s=e=>{this._previousScrollData.visibleEntryTop=e.target.offsetTop,this._process(n(e))},t=(this._rootElement||document.documentElement).scrollTop,o=t>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=t;for(const i of e){if(!i.isIntersecting){this._activeTarget=null,this._clearActiveClass(n(i));continue}const a=i.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(o&&a){if(s(i),!t)return;continue}!o&&!a&&s(i)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const e=t.find(mt,this._config.target);for(const n of e){if(!n.hash||w(n))continue;const s=t.findOne(n.hash,this._element);H(s)&&(this._targetLinks.set(n.hash,n),this._observableSections.set(n.hash,s))}}_process(t){if(this._activeTarget===t)return;this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(q),this._activateParents(t),e.trigger(this._element,Ua,{relatedTarget:t})}_activateParents(e){if(e.classList.contains(Ya)){t.findOne(or,e.closest(sr)).classList.add(q);return}for(const n of t.parents(e,Za))for(const e of t.prev(n,nr))e.classList.add(q)}_clearActiveClass(e){e.classList.remove(q);const n=t.find(`${mt}.${q}`,e);for(const e of n)e.classList.remove(q)}static jQueryInterface(e){return this.each(function(){const t=Ee.getOrCreateInstance(this,e);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()})}}e.on(window,qa,()=>{for(const e of t.find(Xa))Ee.getOrCreateInstance(e)}),c(Ee);const cr="tab",lr="bs.tab",M=`.${lr}`,ur=`hide${M}`,hr=`hidden${M}`,mr=`show${M}`,fr=`shown${M}`,pr=`click${M}`,gr=`keydown${M}`,vr=`load${M}`,br="ArrowLeft",bn="ArrowRight",yr="ArrowUp",yn="ArrowDown",N="active",Mn="fade",We="show",Cr="dropdown",Er=".dropdown-toggle",kr=".dropdown-menu",$e=":not(.dropdown-toggle)",Sr='.list-group, .nav, [role="tablist"]',Mr=".nav-item, .list-group-item",Fr=`.nav-link${$e}, .list-group-item${$e}, [role="tab"]${$e}`,Kn='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',De=`${Fr}, ${Kn}`,Dr=`.${N}[data-bs-toggle="tab"], .${N}[data-bs-toggle="pill"], .${N}[data-bs-toggle="list"]`;class R extends u{constructor(t){if(super(t),this._parent=this._element.closest(Sr),!this._parent)return;this._setInitialAttributes(this._parent,this._getChildren()),e.on(this._element,gr,e=>this._keydown(e))}static get NAME(){return cr}show(){const t=this._element;if(this._elemIsActive(t))return;const n=this._getActiveElem(),s=n?e.trigger(n,ur,{relatedTarget:t}):null,o=e.trigger(t,mr,{relatedTarget:n});if(o.defaultPrevented||s&&s.defaultPrevented)return;this._deactivate(n,t),this._activate(t,n)}_activate(t,n){if(!t)return;t.classList.add(N),this._activate(v(t));const s=()=>{if(t.getAttribute("role")!=="tab"){t.classList.add(We);return}t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),e.trigger(t,fr,{relatedTarget:n})};this._queueCallback(s,t,t.classList.contains(Mn))}_deactivate(t,n){if(!t)return;t.classList.remove(N),t.blur(),this._deactivate(v(t));const s=()=>{if(t.getAttribute("role")!=="tab"){t.classList.remove(We);return}t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),e.trigger(t,hr,{relatedTarget:n})};this._queueCallback(s,t,t.classList.contains(Mn))}_keydown(e){if(![br,bn,yr,yn].includes(e.key))return;e.stopPropagation(),e.preventDefault();const n=[bn,yn].includes(e.key),t=Le(this._getChildren().filter(e=>!w(e)),e.target,n,!0);t&&(t.focus({preventScroll:!0}),R.getOrCreateInstance(t).show())}_getChildren(){return t.find(De,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=v(e);if(!t)return;this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`#${e.id}`)}_toggleDropDown(e,n){const s=this._getOuterElement(e);if(!s.classList.contains(Cr))return;const o=(e,o)=>{const i=t.findOne(e,s);i&&i.classList.toggle(o,n)};o(Er,N),o(kr,We),s.setAttribute("aria-expanded",n)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains(N)}_getInnerElement(e){return e.matches(De)?e:t.findOne(De,e)}_getOuterElement(e){return e.closest(Mr)||e}static jQueryInterface(e){return this.each(function(){const t=R.getOrCreateInstance(this);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()})}}e.on(document,pr,Kn,function(e){if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),w(this))return;R.getOrCreateInstance(this).show()}),e.on(window,vr,()=>{for(const e of t.find(Dr))R.getOrCreateInstance(e)}),c(R);const Lr="toast",Rr="bs.toast",C=`.${Rr}`,Hr=`mouseover${C}`,Ir=`mouseout${C}`,Br=`focusin${C}`,Vr=`focusout${C}`,$r=`hide${C}`,Wr=`hidden${C}`,Ur=`show${C}`,Kr=`shown${C}`,qr="fade",ts="hide",ge="show",we="showing",Qr={animation:"boolean",autohide:"boolean",delay:"number"},Zr={animation:!0,autohide:!0,delay:5e3};class Ce extends u{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Zr}static get DefaultType(){return Qr}static get NAME(){return Lr}show(){const t=e.trigger(this._element,Ur);if(t.defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add(qr);const n=()=>{this._element.classList.remove(we),e.trigger(this._element,Kr),this._maybeScheduleHide()};this._element.classList.remove(ts),ne(this._element),this._element.classList.add(ge,we),this._queueCallback(n,this._element,this._config.animation)}hide(){if(!this.isShown())return;const t=e.trigger(this._element,$r);if(t.defaultPrevented)return;const n=()=>{this._element.classList.add(ts),this._element.classList.remove(we,ge),e.trigger(this._element,Wr)};this._element.classList.add(we),this._queueCallback(n,this._element,this._config.animation)}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ge),super.dispose()}isShown(){return this._element.classList.contains(ge)}_maybeScheduleHide(){if(!this._config.autohide)return;if(this._hasMouseInteraction||this._hasKeyboardInteraction)return;this._timeout=setTimeout(()=>{this.hide()},this._config.delay)}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":{this._hasMouseInteraction=t;break}case"focusin":case"focusout":{this._hasKeyboardInteraction=t;break}}if(t){this._clearTimeout();return}const n=e.relatedTarget;if(this._element===n||this._element.contains(n))return;this._maybeScheduleHide()}_setListeners(){e.on(this._element,Hr,e=>this._onInteraction(e,!0)),e.on(this._element,Ir,e=>this._onInteraction(e,!1)),e.on(this._element,Br,e=>this._onInteraction(e,!0)),e.on(this._element,Vr,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each(function(){const t=Ce.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e](this)}})}}Se(Ce),c(Ce);const ec={Alert:de,Button:fe,Carousel:ie,Collapse:le,Dropdown:h,Modal:B,Offcanvas:A,Popover:ct,ScrollSpy:Ee,Tab:R,Toast:Ce,Tooltip:U};return ec}),function(e){"use strict";e(function(){e('[data-bs-toggle="tooltip"]').tooltip(),e('[data-bs-toggle="popover"]').popover(),e(".popover-dismiss").popover({trigger:"focus"})});function t(e){return e.offset().top+e.outerHeight()}e(function(){var n,o,i,s=e(".js-td-cover");if(!s.length)return;o=t(s),i=e(".js-navbar-scroll").offset().top,n=Math.ceil(e(".js-navbar-scroll").outerHeight()),o-i',t.href="#"+e.id,e.insertAdjacentElement("beforeend",t),e.addEventListener("mouseenter",function(){t.style.visibility="initial"}),e.addEventListener("mouseleave",function(){t.style.visibility="hidden"})}})})}(jQuery),function(e){"use strict";var t={init:function(){e(document).ready(function(){e(document).on("keypress",".td-search input",function(t){if(t.keyCode!==13)return;var n=e(this).val(),s="search/?q="+n;return document.location=s,!1})})}};t.init()}(jQuery) \ No newline at end of file diff --git a/docs/public/js/main.min.6b611378dd7aa9db092fab7032555c3f7cf1f6f0216c4527424189c537230618.js b/docs/public/js/main.min.6b611378dd7aa9db092fab7032555c3f7cf1f6f0216c4527424189c537230618.js deleted file mode 100644 index 54eaa2538..000000000 --- a/docs/public/js/main.min.6b611378dd7aa9db092fab7032555c3f7cf1f6f0216c4527424189c537230618.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v5.2.3 (https://getbootstrap.com/) - * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */(function(e,t){typeof exports=="object"&&typeof module!="undefined"?module.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis!="undefined"?globalThis:e||self,e.bootstrap=t())})(this,function(){"use strict";const ro=1e6,Jr=1e3,ut="transitionend",Xr=e=>e==null?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),Gr=e=>{do e+=Math.floor(Math.random()*ro);while(document.getElementById(e))return e},ss=e=>{let t=e.getAttribute("data-bs-target");if(!t||t==="#"){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&n!=="#"?n.trim():null}return t},ns=e=>{const t=ss(e);return t?document.querySelector(t)?t:null:null},v=e=>{const t=ss(e);return t?document.querySelector(t):null},Yr=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),o=Number.parseFloat(n);return!s&&!o?0:(t=t.split(",")[0],n=n.split(",")[0],(Number.parseFloat(t)+Number.parseFloat(n))*Jr)},es=e=>{e.dispatchEvent(new Event(ut))},g=e=>!!e&&typeof e=="object"&&(typeof e.jquery!="undefined"&&(e=e[0]),typeof e.nodeType!="undefined"),E=e=>g(e)?e.jquery?e[0]:e:typeof e=="string"&&e.length>0?document.querySelector(e):null,H=e=>{if(!g(e)||e.getClientRects().length===0)return!1;const n=getComputedStyle(e).getPropertyValue("visibility")==="visible",t=e.closest("details:not([open])");if(!t)return n;if(t!==e){const n=e.closest("summary");if(n&&n.parentNode!==t)return!1;if(n===null)return!1}return n},w=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(typeof e.disabled!="undefined"?e.disabled:e.hasAttribute("disabled")&&e.getAttribute("disabled")!=="false"),Jn=e=>{if(!document.documentElement.attachShadow)return null;if(typeof e.getRootNode=="function"){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?Jn(e.parentNode):null},Oe=()=>{},ne=e=>{e.offsetHeight},Xn=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,He=[],Pr=e=>{document.readyState==="loading"?(He.length||document.addEventListener("DOMContentLoaded",()=>{for(const e of He)e()}),He.push(e)):e()},a=()=>document.documentElement.dir==="rtl",c=e=>{Pr(()=>{const t=Xn();if(t){const n=e.NAME,s=t.fn[n];t.fn[n]=e.jQueryInterface,t.fn[n].Constructor=e,t.fn[n].noConflict=()=>(t.fn[n]=s,e.jQueryInterface)}})},y=e=>{typeof e=="function"&&e()},Yn=(e,t,n=!0)=>{if(!n){y(e);return}const i=5,a=Yr(t)+i;let s=!1;const o=({target:n})=>{if(n!==t)return;s=!0,t.removeEventListener(ut,o),y(e)};t.addEventListener(ut,o),setTimeout(()=>{s||es(t)},a)},Le=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return o===-1?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])},Nr=/[^.]*(?=\..*)\.|.*/,zr=/\..*/,Tr=/::\d+$/,Ge={};let Un=1;const Vn={mouseenter:"mouseover",mouseleave:"mouseout"},Ar=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function In(e,t){return t&&`${t}::${Un++}`||e.uidEvent||Un++}function Fn(e){const t=In(e);return e.uidEvent=t,Ge[t]=Ge[t]||{},Ge[t]}function xr(t,n){return function s(o){return st(o,{delegateTarget:t}),s.oneOff&&e.off(t,o.type,n),n.apply(t,[o])}}function Or(t,n,s){return function o(i){const a=t.querySelectorAll(n);for(let{target:r}=i;r&&r!==this;r=r.parentNode)for(const c of a){if(c!==r)continue;return st(i,{delegateTarget:r}),o.oneOff&&e.off(t,i.type,n,s),s.apply(r,[i])}}}function Sn(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function En(e,t,n){const o=typeof t=="string",i=o?n:t||n;let s=On(e);return Ar.has(s)||(s=e),[o,i,s]}function Cn(e,t,n,s,o){if(typeof t!="string"||!e)return;let[r,i,c]=En(t,n,s);if(t in Vn){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};i=e(i)}const d=Fn(e),u=d[c]||(d[c]={}),l=Sn(u,i,r?n:null);if(l){l.oneOff=l.oneOff&&o;return}const h=In(i,t.replace(Nr,"")),a=r?Or(e,n,i):xr(e,i);a.delegationSelector=r?n:null,a.callable=i,a.oneOff=o,a.uidEvent=h,u[h]=a,e.addEventListener(c,a,r)}function Me(e,t,n,s,o){const i=Sn(t[n],s,o);if(!i)return;e.removeEventListener(n,i,Boolean(o)),delete t[n][i.uidEvent]}function wr(e,t,n,s){const o=t[n]||{};for(const i of Object.keys(o))if(i.includes(s)){const s=o[i];Me(e,t,n,s.callable,s.delegationSelector)}}function On(e){return e=e.replace(zr,""),Vn[e]||e}const e={on(e,t,n,s){Cn(e,t,n,s,!1)},one(e,t,n,s){Cn(e,t,n,s,!0)},off(e,t,n,s){if(typeof t!="string"||!e)return;const[c,r,i]=En(t,n,s),l=i!==t,o=Fn(e),a=o[i]||{},d=t.startsWith(".");if(typeof r!="undefined"){if(!Object.keys(a).length)return;Me(e,o,i,r,c?n:null);return}if(d)for(const n of Object.keys(o))wr(e,o,n,t.slice(1));for(const n of Object.keys(a)){const s=n.replace(Tr,"");if(!l||t.includes(s)){const t=a[n];Me(e,o,i,t.callable,t.delegationSelector)}}},trigger(e,t,n){if(typeof t!="string"||!e)return null;const i=Xn(),l=On(t),d=t!==l;let s=null,a=!0,r=!0,c=!1;d&&i&&(s=i.Event(t,n),i(e).trigger(s),a=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),c=s.isDefaultPrevented());let o=new Event(t,{bubbles:a,cancelable:!0});return o=st(o,n),c&&o.preventDefault(),r&&e.dispatchEvent(o),o.defaultPrevented&&s&&s.preventDefault(),o}};function st(e,t){for(const[n,s]of Object.entries(t||{}))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get(){return s}})}return e}const x=new Map,it={set(e,t,n){x.has(e)||x.set(e,new Map);const s=x.get(e);if(!s.has(t)&&s.size!==0){console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`);return}s.set(t,n)},get(e,t){return x.has(e)?x.get(e).get(t)||null:null},remove(e,t){if(!x.has(e))return;const n=x.get(e);n.delete(t),n.size===0&&x.delete(e)}};function _n(e){if(e==="true")return!0;if(e==="false")return!1;if(e===Number(e).toString())return Number(e);if(e===""||e==="null")return null;if(typeof e!="string")return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function ht(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const b={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${ht(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${ht(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const o of n){let s=o.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),t[s]=_n(e.dataset[o])}return t},getDataAttribute(e,t){return _n(e.getAttribute(`data-bs-${ht(t)}`))}};class te{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=g(t)?b.getDataAttribute(t,"config"):{};return{...this.constructor.Default,...typeof n=="object"?n:{},...g(t)?b.getDataAttributes(t):{},...typeof e=="object"?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const n of Object.keys(t)){const s=t[n],o=e[n],i=g(o)?"element":Xr(o);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const _r="5.2.3";class u extends te{constructor(e,t){if(super(),e=E(e),!e)return;this._element=e,this._config=this._getConfig(t),it.set(this._element,this.constructor.DATA_KEY,this)}dispose(){it.remove(this._element,this.constructor.DATA_KEY),e.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){Yn(e,t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return it.get(E(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,typeof t=="object"?t:null)}static get VERSION(){return _r}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const Se=(t,n="hide")=>{const o=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;e.on(document,o,`[data-bs-dismiss="${s}"]`,function(e){if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),w(this))return;const o=v(this)||this.closest(`.${s}`),i=t.getOrCreateInstance(o);i[n]()})},jr="alert",dr="bs.alert",vn=`.${dr}`,rr=`close${vn}`,Ja=`closed${vn}`,Qa="fade",Ga="show";class de extends u{static get NAME(){return jr}close(){const t=e.trigger(this._element,rr);if(t.defaultPrevented)return;this._element.classList.remove(Ga);const n=this._element.classList.contains(Qa);this._queueCallback(()=>this._destroyElement(),this._element,n)}_destroyElement(){this._element.remove(),e.trigger(this._element,Ja),this.dispose()}static jQueryInterface(e){return this.each(function(){const t=de.getOrCreateInstance(this);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)})}}Se(de,"close"),c(de);const Ka="button",$a="bs.button",Ia=`.${$a}`,Da=".data-api",ba="active",fn='[data-bs-toggle="button"]',va=`click${Ia}${Da}`;class fe extends u{static get NAME(){return Ka}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle(ba))}static jQueryInterface(e){return this.each(function(){const t=fe.getOrCreateInstance(this);e==="toggle"&&t[e]()})}}e.on(document,va,fn,e=>{e.preventDefault();const t=e.target.closest(fn),n=fe.getOrCreateInstance(t);n.toggle()}),c(fe);const t={find(e,t=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(t,e))},findOne(e,t=document.documentElement){return Element.prototype.querySelector.call(t,e)},children(e,t){return[].concat(...e.children).filter(e=>e.matches(t))},parents(e,t){const s=[];let n=e.parentNode.closest(t);for(;n;)s.push(n),n=n.parentNode.closest(t);return s},prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!w(e)&&H(e))}},ga="swipe",$=".bs.swipe",pa=`touchstart${$}`,ma=`touchmove${$}`,ua=`touchend${$}`,na=`pointerdown${$}`,Xi=`pointerup${$}`,$i="touch",Vi="pen",Pi="pointer-event",Li=40,Ni={endCallback:null,leftCallback:null,rightCallback:null},Di={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Re extends te{constructor(e,t){if(super(),this._element=e,!e||!Re.isSupported())return;this._config=this._getConfig(t),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents()}static get Default(){return Ni}static get DefaultType(){return Di}static get NAME(){return ga}dispose(){e.off(this._element,$)}_start(e){if(!this._supportPointerEvents){this._deltaX=e.touches[0].clientX;return}this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX),this._handleSwipe(),y(this._config.endCallback)}_move(e){this._deltaX=e.touches&&e.touches.length>1?0:e.touches[0].clientX-this._deltaX}_handleSwipe(){const e=Math.abs(this._deltaX);if(e<=Li)return;const t=e/this._deltaX;if(this._deltaX=0,!t)return;y(t>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(e.on(this._element,na,e=>this._start(e)),e.on(this._element,Xi,e=>this._end(e)),this._element.classList.add(Pi)):(e.on(this._element,pa,e=>this._start(e)),e.on(this._element,ma,e=>this._move(e)),e.on(this._element,ua,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&(e.pointerType===Vi||e.pointerType===$i)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const is="carousel",Mi="bs.carousel",k=`.${Mi}`,Wt=".data-api",ki="ArrowLeft",yi="ArrowRight",ji=500,Z="next",K="prev",P="left",Ae="right",vi=`slide${k}`,Ve=`slid${k}`,di=`keydown${k}`,li=`mouseenter${k}`,oi=`mouseleave${k}`,ti=`dragstart${k}`,Zo=`load${k}${Wt}`,qo=`click${k}${Wt}`,Lt="carousel",ye="active",Wo="slide",Bo="carousel-item-end",Io="carousel-item-start",Po="carousel-item-next",Ro="carousel-item-prev",Mt=".active",Ct=".carousel-item",Lo=Mt+Ct,No=".carousel-item img",Do=".carousel-indicators",zo="[data-bs-slide], [data-bs-slide-to]",To='[data-bs-ride="carousel"]',bo={[ki]:Ae,[yi]:P},go={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},lo={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ie extends u{constructor(e,n){super(e,n),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=t.findOne(Do,this._element),this._addEventListeners(),this._config.ride===Lt&&this.cycle()}static get Default(){return go}static get DefaultType(){return lo}static get NAME(){return is}next(){this._slide(Z)}nextWhenVisible(){!document.hidden&&H(this._element)&&this.next()}prev(){this._slide(K)}pause(){this._isSliding&&es(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){if(!this._config.ride)return;if(this._isSliding){e.one(this._element,Ve,()=>this.cycle());return}this.cycle()}to(t){const n=this._getItems();if(t>n.length-1||t<0)return;if(this._isSliding){e.one(this._element,Ve,()=>this.to(t));return}const s=this._getItemIndex(this._getActive());if(s===t)return;const o=t>s?Z:K;this._slide(o,n[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(e){return e.defaultInterval=e.interval,e}_addEventListeners(){this._config.keyboard&&e.on(this._element,di,e=>this._keydown(e)),this._config.pause==="hover"&&(e.on(this._element,li,()=>this.pause()),e.on(this._element,oi,()=>this._maybeEnableCycle())),this._config.touch&&Re.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const n of t.find(No,this._element))e.on(n,ti,e=>e.preventDefault());const n=()=>{if(this._config.pause!=="hover")return;this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),ji+this._config.interval)},s={leftCallback:()=>this._slide(this._directionToOrder(P)),rightCallback:()=>this._slide(this._directionToOrder(Ae)),endCallback:n};this._swipeHelper=new Re(this._element,s)}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=bo[e.key];t&&(e.preventDefault(),this._slide(this._directionToOrder(t)))}_getItemIndex(e){return this._getItems().indexOf(e)}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const s=t.findOne(Mt,this._indicatorsElement);s.classList.remove(ye),s.removeAttribute("aria-current");const n=t.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add(ye),n.setAttribute("aria-current","true"))}_updateInterval(){const e=this._activeElement||this._getActive();if(!e)return;const t=Number.parseInt(e.getAttribute("data-bs-interval"),10);this._config.interval=t||this._config.defaultInterval}_slide(t,n=null){if(this._isSliding)return;const o=this._getActive(),a=t===Z,s=n||Le(this._getItems(),o,a,this._config.wrap);if(s===o)return;const c=this._getItemIndex(s),l=n=>e.trigger(this._element,n,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(o),to:c}),d=l(vi);if(d.defaultPrevented)return;if(!o||!s)return;const u=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(c),this._activeElement=s;const i=a?Io:Bo,r=a?Po:Ro;s.classList.add(r),ne(s),o.classList.add(i),s.classList.add(i);const h=()=>{s.classList.remove(i,r),s.classList.add(ye),o.classList.remove(ye,r,i),this._isSliding=!1,l(Ve)};this._queueCallback(h,o,this._isAnimated()),u&&this.cycle()}_isAnimated(){return this._element.classList.contains(Wo)}_getActive(){return t.findOne(Lo,this._element)}_getItems(){return t.find(Ct,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(e){return a()?e===P?K:Z:e===P?Z:K}_orderToDirection(e){return a()?e===K?P:Ae:e===K?Ae:P}static jQueryInterface(e){return this.each(function(){const t=ie.getOrCreateInstance(this,e);if(typeof e=="number"){t.to(e);return}if(typeof e=="string"){if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()}})}}e.on(document,qo,zo,function(e){const n=v(this);if(!n||!n.classList.contains(Lt))return;e.preventDefault();const t=ie.getOrCreateInstance(n),s=this.getAttribute("data-bs-slide-to");if(s){t.to(s),t._maybeEnableCycle();return}if(b.getDataAttribute(this,"slide")==="next"){t.next(),t._maybeEnableCycle();return}t.prev(),t._maybeEnableCycle()}),e.on(window,Zo,()=>{const e=t.find(To);for(const t of e)ie.getOrCreateInstance(t)}),c(ie);const os="collapse",ao="bs.collapse",ee=`.${ao}`,eo=".data-api",Zs=`show${ee}`,Xs=`shown${ee}`,Gs=`hide${ee}`,qs=`hidden${ee}`,Ks=`click${ee}${eo}`,rt="show",V="collapse",je="collapsing",Us="collapsed",Ws=`:scope .${V} .${V}`,$s="collapse-horizontal",Vs="width",Bs="height",Is=".collapse.show, .collapse.collapsing",et='[data-bs-toggle="collapse"]',Hs={parent:null,toggle:!0},Ps={parent:"(null|element)",toggle:"boolean"};class le extends u{constructor(e,n){super(e,n),this._isTransitioning=!1,this._triggerArray=[];const s=t.find(et);for(const e of s){const n=ns(e),o=t.find(n).filter(e=>e===this._element);n!==null&&o.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Hs}static get DefaultType(){return Ps}static get NAME(){return os}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let n=[];if(this._config.parent&&(n=this._getFirstLevelChildren(Is).filter(e=>e!==this._element).map(e=>le.getOrCreateInstance(e,{toggle:!1}))),n.length&&n[0]._isTransitioning)return;const s=e.trigger(this._element,Zs);if(s.defaultPrevented)return;for(const e of n)e.hide();const t=this._getDimension();this._element.classList.remove(V),this._element.classList.add(je),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const o=()=>{this._isTransitioning=!1,this._element.classList.remove(je),this._element.classList.add(V,rt),this._element.style[t]="",e.trigger(this._element,Xs)},i=t[0].toUpperCase()+t.slice(1),a=`scroll${i}`;this._queueCallback(o,this._element,!0),this._element.style[t]=`${this._element[a]}px`}hide(){if(this._isTransitioning||!this._isShown())return;const n=e.trigger(this._element,Gs);if(n.defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,ne(this._element),this._element.classList.add(je),this._element.classList.remove(V,rt);for(const e of this._triggerArray){const t=v(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0;const s=()=>{this._isTransitioning=!1,this._element.classList.remove(je),this._element.classList.add(V),e.trigger(this._element,qs)};this._element.style[t]="",this._queueCallback(s,this._element,!0)}_isShown(e=this._element){return e.classList.contains(rt)}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=E(e.parent),e}_getDimension(){return this._element.classList.contains($s)?Vs:Bs}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(et);for(const t of e){const n=v(t);n&&this._addAriaAndCollapsedClass([t],this._isShown(n))}}_getFirstLevelChildren(e){const n=t.find(Ws,this._config.parent);return t.find(e,this._config.parent).filter(e=>!n.includes(e))}_addAriaAndCollapsedClass(e,t){if(!e.length)return;for(const n of e)n.classList.toggle(Us,!t),n.setAttribute("aria-expanded",t)}static jQueryInterface(e){const t={};return typeof e=="string"&&/show|hide/.test(e)&&(t.toggle=!1),this.each(function(){const n=le.getOrCreateInstance(this,t);if(typeof e=="string"){if(typeof n[e]=="undefined")throw new TypeError(`No method named "${e}"`);n[e]()}})}}e.on(document,Ks,et,function(e){(e.target.tagName==="A"||e.delegateTarget&&e.delegateTarget.tagName==="A")&&e.preventDefault();const n=ns(this),s=t.find(n);for(const e of s)le.getOrCreateInstance(e,{toggle:!1}).toggle()}),c(le);var O,z,s="top",se,Ln,Bn,ae,Gn,Qn,Je,St,At,kt,Et,he,o="bottom",i="right",n="left",xe="auto",G=[s,o,i,n],F="start",W="end",$t="clippingParents",ze="viewport",L="popper",Kt="reference",Ie=G.reduce(function(e,t){return e.concat([t+"-"+F,t+"-"+W])},[]),Be=[].concat(G,[xe]).reduce(function(e,t){return e.concat([t,t+"-"+F,t+"-"+W])},[]),Gt="beforeRead",Xt="read",Qt="afterRead",Zt="beforeMain",Jt="main",en="afterMain",tn="beforeWrite",nn="write",sn="afterWrite",on=[Gt,Xt,Qt,Zt,Jt,en,tn,nn,sn];function f(e){return e?(e.nodeName||"").toLowerCase():null}function r(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t?t.defaultView||window:window}return e}function D(e){var t=r(e).Element;return e instanceof t||e instanceof Element}function l(e){var t=r(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}function Ye(e){if(typeof ShadowRoot=="undefined")return!1;var t=r(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}function Ss(e){var t=e.state;Object.keys(t.elements).forEach(function(e){var o=t.styles[e]||{},s=t.attributes[e]||{},n=t.elements[e];if(!l(n)||!f(n))return;Object.assign(n.style,o),Object.keys(s).forEach(function(e){var t=s[e];t===!1?n.removeAttribute(e):n.setAttribute(e,t===!0?"":t)})})}function Es(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(e){var s=t.elements[e],o=t.attributes[e]||{},i=Object.keys(t.styles.hasOwnProperty(e)?t.styles[e]:n[e]),a=i.reduce(function(e,t){return e[t]="",e},{});if(!l(s)||!f(s))return;Object.assign(s.style,a),Object.keys(o).forEach(function(e){s.removeAttribute(e)})})}}const Ze={name:"applyStyles",enabled:!0,phase:"write",fn:Ss,effect:Es,requires:["computeStyles"]};function m(e){return e.split("-")[0]}O=Math.max,se=Math.min,z=Math.round;function nt(){var e=navigator.userAgentData;return e!=null&&e.brands?e.brands.map(function(e){return e.brand+"/"+e.version}).join(" "):navigator.userAgent}function jn(){return!/^((?!chrome|android).)*safari/i.test(nt())}function X(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!1),s=e.getBoundingClientRect(),o=1,i=1,t&&l(e)&&(o=e.offsetWidth>0?z(s.width)/e.offsetWidth||1:1,i=e.offsetHeight>0?z(s.height)/e.offsetHeight||1:1);var s,o,i,f=D(e)?r(e):window,a=f.visualViewport,u=!jn()&&n,c=(s.left+(u&&a?a.offsetLeft:0))/o,d=(s.top+(u&&a?a.offsetTop:0))/i,h=s.width/o,m=s.height/i;return{width:h,height:m,top:d,right:c+h,bottom:d+m,left:c,x:c,y:d}}function dt(e){var t=X(e),n=e.offsetWidth,s=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-s)<=1&&(s=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:s}}function wn(e,t){var n,s=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(s&&Ye(s)){n=t;do{if(n&&e.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function p(e){return r(e).getComputedStyle(e)}function xs(e){return["table","td","th"].indexOf(f(e))>=0}function _(e){return((D(e)?e.ownerDocument:e.document)||window.document).documentElement}function ve(e){return f(e)==="html"?e:e.assignedSlot||e.parentNode||(Ye(e)?e.host:null)||_(e)}function kn(e){return!l(e)||p(e).position==="fixed"?null:e.offsetParent}function _s(e){var t,n,o,s=/firefox/i.test(nt()),i=/Trident/i.test(nt());if(i&&l(e)&&(o=p(e),o.position==="fixed"))return null;for(t=ve(e),Ye(t)&&(t=t.host);l(t)&&["html","body"].indexOf(f(t))<0;){if(n=p(t),n.transform!=="none"||n.perspective!=="none"||n.contain==="paint"||["transform","perspective"].indexOf(n.willChange)!==-1||s&&n.willChange==="filter"||s&&n.filter&&n.filter!=="none")return t;t=t.parentNode}return null}function re(e){for(var n=r(e),t=kn(e);t&&xs(t)&&p(t).position==="static";)t=kn(t);return t&&(f(t)==="html"||f(t)==="body"&&p(t).position==="static")?n:t||_s(e)||n}function Ue(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function oe(e,t,n){return O(e,se(t,n))}function js(e,t,n){var s=oe(e,t,n);return s>n?n:s}function zn(){return{top:0,right:0,bottom:0,left:0}}function Dn(e){return Object.assign({},zn(),e)}function Nn(e,t){return t.reduce(function(t,n){return t[n]=e,t},{})}Ln=function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,Dn(typeof t!="number"?t:Nn(t,G))};function bs(e){var r,c,d,u,p,g,v,b,j,y,_,O,x,C,E,t=e.state,S=e.name,A=e.options,h=t.elements.arrow,f=t.modifiersData.popperOffsets,w=m(t.placement),a=Ue(w),k=[n,i].indexOf(w)>=0,l=k?"height":"width";if(!h||!f)return;g=Ln(A.padding,t),v=dt(h),b=a==="y"?s:n,j=a==="y"?o:i,y=t.rects.reference[l]+t.rects.reference[a]-f[a]-t.rects.popper[l],_=f[a]-t.rects.reference[a],c=re(h),p=c?a==="y"?c.clientHeight||0:c.clientWidth||0:0,O=y/2-_/2,x=g[b],C=p-v[l]-g[j],u=p/2-v[l]/2+O,d=oe(x,u,C),E=a,t.modifiersData[S]=(r={},r[E]=d,r.centerOffset=d-u,r)}function vs(e){var n=e.state,o=e.options,s=o.element,t=s===void 0?"[data-popper-arrow]":s;if(t==null)return;if(typeof t=="string"&&(t=n.elements.popper.querySelector(t),!t))return;if(!wn(n.elements.popper,t))return;n.elements.arrow=t}const Hn={name:"arrow",enabled:!0,phase:"main",fn:bs,effect:vs,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Q(e){return e.split("-")[1]}Bn={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ms(e){var n=e.x,s=e.y,o=window,t=o.devicePixelRatio||1;return{x:z(n*t)/t||0,y:z(s*t)/t||0}}function $n(e){var c,u,h,b,j,y,x,T,z,f=e.popper,D=e.popperRect,d=e.placement,k=e.variation,m=e.offsets,S=e.position,v=e.gpuAcceleration,A=e.adaptive,w=e.roundOffsets,L=e.isFixed,N=m.x,t=N===void 0?0:N,M=m.y,a=M===void 0?0:M,E=typeof w=="function"?w({x:t,y:a}):{x:t,y:a},t=E.x,a=E.y,F=m.hasOwnProperty("x"),C=m.hasOwnProperty("y"),O=n,g=s,l=window;return A&&(c=re(f),y="clientHeight",j="clientWidth",c===r(f)&&(c=_(f),p(c).position!=="static"&&S==="absolute"&&(y="scrollHeight",j="scrollWidth")),c=c,(d===s||(d===n||d===i)&&k===W)&&(g=o,T=L&&c===l&&l.visualViewport?l.visualViewport.height:c[y],a-=T-D.height,a*=v?1:-1),(d===n||(d===s||d===o)&&k===W)&&(O=i,z=L&&c===l&&l.visualViewport?l.visualViewport.width:c[j],t-=z-D.width,t*=v?1:-1)),x=Object.assign({position:S},A&&Bn),b=w===!0?ms({x:t,y:a}):{x:t,y:a},t=b.x,a=b.y,v?Object.assign({},x,(h={},h[g]=C?"0":"",h[O]=F?"0":"",h.transform=(l.devicePixelRatio||1)<=1?"translate("+t+"px, "+a+"px)":"translate3d("+t+"px, "+a+"px, 0)",h)):Object.assign({},x,(u={},u[g]=C?a+"px":"",u[O]=F?t+"px":"",u.transform="",u))}function hs(e){var t=e.state,n=e.options,s=n.gpuAcceleration,c=s===void 0||s,o=n.adaptive,l=o===void 0||o,i=n.roundOffsets,a=i===void 0||i,r={placement:m(t.placement),variation:Q(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:c,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,$n(Object.assign({},r,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:l,roundOffsets:a})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,$n(Object.assign({},r,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:a})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}const Te={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:hs,data:{}};ae={passive:!0};function ls(e){var n=e.state,t=e.instance,s=e.options,o=s.scroll,i=o===void 0||o,a=s.resize,c=a===void 0||a,l=r(n.elements.popper),d=[].concat(n.scrollParents.reference,n.scrollParents.popper);return i&&d.forEach(function(e){e.addEventListener("scroll",t.update,ae)}),c&&l.addEventListener("resize",t.update,ae),function(){i&&d.forEach(function(e){e.removeEventListener("scroll",t.update,ae)}),c&&l.removeEventListener("resize",t.update,ae)}}const Pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:ls,data:{}};Gn={left:"right",right:"left",bottom:"top",top:"bottom"};function ke(e){return e.replace(/left|right|bottom|top/g,function(e){return Gn[e]})}Qn={start:"end",end:"start"};function Zn(e){return e.replace(/start|end/g,function(e){return Qn[e]})}function Ke(e){var t=r(e),n=t.pageXOffset,s=t.pageYOffset;return{scrollLeft:n,scrollTop:s}}function qe(e){return X(_(e)).left+Ke(e).scrollLeft}function rs(e,t){var s,d=r(e),o=_(e),n=d.visualViewport,i=o.clientWidth,a=o.clientHeight,c=0,l=0;return n&&(i=n.width,a=n.height,s=jn(),(s||!s&&t==="fixed")&&(c=n.offsetLeft,l=n.offsetTop)),{width:i,height:a,x:c+qe(e),y:l}}function Fi(e){var s,n=_(e),o=Ke(e),t=(s=e.ownerDocument)==null?void 0:s.body,i=O(n.scrollWidth,n.clientWidth,t?t.scrollWidth:0,t?t.clientWidth:0),r=O(n.scrollHeight,n.clientHeight,t?t.scrollHeight:0,t?t.clientHeight:0),a=-o.scrollLeft+qe(e),c=-o.scrollTop;return p(t||n).direction==="rtl"&&(a+=O(n.clientWidth,t?t.clientWidth:0)-i),{width:i,height:r,x:a,y:c}}function at(e){var t=p(e),n=t.overflow,s=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+s)}function zt(e){return["html","body","#document"].indexOf(f(e))>=0?e.ownerDocument.body:l(e)&&at(e)?e:zt(ve(e))}function ce(e,t){t===void 0&&(t=[]);var s,n=zt(e),o=n===((s=e.ownerDocument)==null?void 0:s.body),i=r(n),a=o?[i].concat(i.visualViewport||[],at(n)?n:[]):n,c=t.concat(a);return o?c:c.concat(ce(ve(a)))}function Fe(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function cs(e,t){var n=X(e,!1,t==="fixed");return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}function qn(e,t,n){return t===ze?Fe(rs(e,n)):D(t)?cs(t,n):Fe(Fi(_(e)))}function ds(e){var n=ce(ve(e)),s=["absolute","fixed"].indexOf(p(e).position)>=0,t=s&&l(e)?re(e):e;return D(t)?n.filter(function(e){return D(e)&&wn(e,t)&&f(e)!=="body"}):[]}function us(e,t,n,s){var a=t==="clippingParents"?ds(e):[].concat(t),i=[].concat(a,[n]),r=i[0],o=i.reduce(function(t,n){var o=qn(e,n,s);return t.top=O(o.top,t.top),t.right=se(o.right,t.right),t.bottom=se(o.bottom,t.bottom),t.left=O(o.left,t.left),t},qn(e,r,s));return o.width=o.right-o.left,o.height=o.bottom-o.top,o.x=o.left,o.y=o.top,o}function Wn(e){var a,r,l,t=e.reference,c=e.element,d=e.placement,u=d?m(d):null,p=d?Q(d):null,h=t.x+t.width/2-c.width/2,f=t.y+t.height/2-c.height/2;switch(u){case s:a={x:h,y:t.y-c.height};break;case o:a={x:h,y:t.y+t.height};break;case i:a={x:t.x+t.width,y:f};break;case n:a={x:t.x-c.width,y:f};break;default:a={x:t.x,y:t.y}}if(r=u?Ue(u):null,r!=null)switch(l=r==="y"?"height":"width",p){case F:a[r]=a[r]-(t[l]/2-c[l]/2);break;case W:a[r]=a[r]+(t[l]/2-c[l]/2);break}return a}function I(e,t){t===void 0&&(t={});var w,n=t,v=n.placement,j=v===void 0?e.placement:v,f=n.strategy,T=f===void 0?e.strategy:f,p=n.boundary,E=p===void 0?$t:p,x=n.rootBoundary,F=x===void 0?ze:x,C=n.elementContext,c=C===void 0?L:C,m=n.altBoundary,M=m!==void 0&&m,b=n.padding,d=b===void 0?0:b,a=Dn(typeof d!="number"?d:Nn(d,G)),S=c===L?Kt:L,O=e.rects.popper,h=e.elements[M?S:c],r=us(D(h)?h:h.contextElement||_(e.elements.popper),E,F,T),y=X(e.elements.reference),k=Wn({reference:y,element:O,strategy:"absolute",placement:j}),A=Fe(Object.assign({},O,k)),l=c===L?A:y,u={top:r.top-l.top+a.top,bottom:l.bottom-r.bottom+a.bottom,left:r.left-l.left+a.left,right:l.right-r.right+a.right},g=e.modifiersData.offset;return c===L&&g&&(w=g[j],Object.keys(u).forEach(function(e){var t=[i,o].indexOf(e)>=0?1:-1,n=[s,o].indexOf(e)>=0?"y":"x";u[e]+=w[n]*t})),u}function fs(e,t){t===void 0&&(t={});var s,n=t,c=n.placement,l=n.boundary,d=n.rootBoundary,u=n.padding,h=n.flipVariations,i=n.allowedAutoPlacements,f=i===void 0?Be:i,a=Q(c),r=a?h?Ie:Ie.filter(function(e){return Q(e)===a}):G,o=r.filter(function(e){return f.indexOf(e)>=0});return o.length===0&&(o=r),s=o.reduce(function(t,n){return t[n]=I(e,{placement:n,boundary:l,rootBoundary:d,padding:u})[m(n)],t},{}),Object.keys(s).sort(function(e,t){return s[e]-s[t]})}function ps(e){if(m(e)===xe)return[];var t=ke(e);return[Zn(e),t,Zn(t)]}function gs(e){var t=e.state,a=e.options,C=e.name;if(t.modifiersData[C]._skip)return;for(var r,c,l,u,h,g,v,y,_,x,E,k,z,M=a.mainAxis,H=M===void 0||M,D=a.altAxis,R=D===void 0||D,L=a.fallbackPlacements,N=a.padding,w=a.boundary,O=a.rootBoundary,B=a.altBoundary,T=a.flipVariations,j=T===void 0||T,V=a.allowedAutoPlacements,d=t.options.placement,U=m(d),P=U===d,K=L||(P||!j?[ke(d)]:ps(d)),p=[d].concat(K).reduce(function(e,n){return e.concat(m(n)===xe?fs(t,{placement:n,boundary:w,rootBoundary:O,padding:N,flipVariations:j,allowedAutoPlacements:V}):n)},[]),W=t.rects.reference,$=t.rects.popper,A=new Map,S=!0,f=p[0],b=0;b=0,_=y?"width":"height",h=I(t,{placement:r,boundary:w,rootBoundary:O,altBoundary:B,padding:N}),l=y?g?i:n:g?o:s,W[_]>$[_]&&(l=ke(l)),z=ke(l),c=[],H&&c.push(h[v]<=0),R&&c.push(h[l]<=0,h[z]<=0),c.every(function(e){return e})){f=r,S=!1;break}A.set(r,c)}if(S){k=j?3:1,E=function(t){var n=p.find(function(e){var n=A.get(e);if(n)return n.slice(0,t).every(function(e){return e})});if(n)return f=n,"break"};for(u=k;u>0;u--)if(x=E(u),x==="break")break}t.placement!==f&&(t.modifiersData[C]._skip=!0,t.placement=f,t.reset=!0)}const Pn={name:"flip",enabled:!0,phase:"main",fn:gs,requiresIfExists:["offset"],data:{_skip:!1}};function Rn(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function Tn(e){return[s,i,o,n].some(function(t){return e[t]>=0})}function ys(e){var t=e.state,a=e.name,r=t.rects.reference,c=t.rects.popper,l=t.modifiersData.preventOverflow,d=I(t,{elementContext:"reference"}),u=I(t,{altBoundary:!0}),n=Rn(d,r),s=Rn(u,c,l),o=Tn(n),i=Tn(s);t.modifiersData[a]={referenceClippingOffsets:n,popperEscapeOffsets:s,isReferenceHidden:o,hasPopperEscaped:i},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":o,"data-popper-escaped":i})}const An={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:ys};function ws(e,t,o){var c=m(e),d=[n,s].indexOf(c)>=0?-1:1,l=typeof o=="function"?o(Object.assign({},t,{placement:e})):o,a=l[0],r=l[1],a=a||0,r=(r||0)*d;return[n,i].indexOf(c)>=0?{x:r,y:a}:{x:a,y:r}}function Os(e){var t=e.state,i=e.options,a=e.name,n=i.offset,r=n===void 0?[0,0]:n,s=Be.reduce(function(e,n){return e[n]=ws(n,t.rects,r),e},{}),o=s[t.placement],c=o.x,l=o.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=c,t.modifiersData.popperOffsets.y+=l),t.modifiersData[a]=s}const xn={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:Os};function Cs(e){var t=e.state,n=e.name;t.modifiersData[n]=Wn({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}const Qe={name:"popperOffsets",enabled:!0,phase:"read",fn:Cs,data:{}};function ks(e){return e==="x"?"y":"x"}function As(e){var r,c,h,p,v,w,C,k,A,M,T,z,D,L,R,P,H,B,V,$,W,U,K,q,Y,G,X,Z,t=e.state,l=e.options,be=e.name,pe=l.mainAxis,ge=pe===void 0||pe,ne=l.altAxis,we=ne!==void 0&&ne,_e=l.boundary,ye=l.rootBoundary,ve=l.altBoundary,je=l.padding,de=l.tether,d=de===void 0||de,ae=l.tetherOffset,S=ae===void 0?0:ae,fe,ue,ee,te,ie,ce,le,me,x=I(t,{boundary:_e,rootBoundary:ye,padding:je,altBoundary:ve}),J=m(t.placement),E=Q(t.placement),he=!E,a=Ue(J),j=ks(a),b=t.modifiersData.popperOffsets,u=t.rects.reference,g=t.rects.popper,_=typeof S=="function"?S(Object.assign({},t.rects,{placement:t.placement})):S,f=typeof _=="number"?{mainAxis:_,altAxis:_}:Object.assign({mainAxis:0,altAxis:0},_),y=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,N={x:0,y:0};if(!b)return;ge&&(R=a==="y"?s:n,P=a==="y"?o:i,r=a==="y"?"height":"width",h=b[a],V=h+x[R],$=h-x[P],W=d?-g[r]/2:0,Z=E===F?u[r]:g[r],X=E===F?-g[r]:-u[r],q=t.elements.arrow,ue=d&&q?dt(q):{width:0,height:0},k=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:zn(),K=k[R],U=k[P],v=oe(0,u[r],ue[r]),ee=he?u[r]/2-W-v-K-f.mainAxis:Z-v-K-f.mainAxis,te=he?-u[r]/2+W+v+U+f.mainAxis:X+v+U+f.mainAxis,C=t.elements.arrow&&re(t.elements.arrow),ie=C?a==="y"?C.clientTop||0:C.clientLeft||0:0,B=(fe=y?.[a])!=null?fe:0,ce=h+ee-B-ie,le=h+te-B,H=oe(d?se(V,ce):V,h,d?O($,le):$),b[a]=H,N[a]=H-h),we&&(Y=a==="x"?s:n,me=a==="x"?o:i,c=b[j],p=j==="y"?"height":"width",L=c+x[Y],D=c-x[me],w=[s,n].indexOf(J)!==-1,z=(G=y?.[j])!=null?G:0,T=w?L:c-u[p]-g[p]-z+f.altAxis,M=w?c+u[p]+g[p]-z-f.altAxis:D,A=d&&w?js(T,c,M):oe(d?T:L,c,d?M:D),b[j]=A,N[j]=A-c),t.modifiersData[be]=N}const un={name:"preventOverflow",enabled:!0,phase:"main",fn:As,requiresIfExists:["offset"]};function Ms(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}function Fs(e){return e===r(e)||!l(e)?Ke(e):Ms(e)}function Ts(e){var t=e.getBoundingClientRect(),n=z(t.width)/e.offsetWidth||1,s=z(t.height)/e.offsetHeight||1;return n!==1||s!==1}function zs(e,t,n){n===void 0&&(n=!1);var r=l(t),c=l(t)&&Ts(t),i=_(t),o=X(e,c,n),a={scrollLeft:0,scrollTop:0},s={x:0,y:0};return(r||!r&&!n)&&((f(t)!=="body"||at(i))&&(a=Fs(t)),l(t)?(s=X(t,!0),s.x+=t.clientLeft,s.y+=t.clientTop):i&&(s.x=qe(i))),{x:o.left+a.scrollLeft-s.x,y:o.top+a.scrollTop-s.y,width:o.width,height:o.height}}function Ds(e){var n=new Map,t=new Set,s=[];e.forEach(function(e){n.set(e.name,e)});function o(e){t.add(e.name);var i=[].concat(e.requires||[],e.requiresIfExists||[]);i.forEach(function(e){if(!t.has(e)){var s=n.get(e);s&&o(s)}}),s.push(e)}return e.forEach(function(e){t.has(e.name)||o(e)}),s}function Ns(e){var t=Ds(e);return on.reduce(function(e,n){return e.concat(t.filter(function(e){return e.phase===n}))},[])}function Ls(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}function Rs(e){var t=e.reduce(function(e,t){var n=e[t.name];return e[t.name]=n?Object.assign({},n,t,{options:Object.assign({},n.options,t.options),data:Object.assign({},n.data,t.data)}):t,e},{});return Object.keys(t).map(function(e){return t[e]})}Je={placement:"bottom",modifiers:[],strategy:"absolute"};function Tt(){for(var t=arguments.length,n=new Array(t),e=0;eNumber.parseInt(e,10)):typeof e=="function"?t=>e(t,this._element):e}_getPopperConfig(){const e={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||this._config.display==="static")&&(b.setDataAttribute(this._menu,"popper","static"),e.modifiers=[{name:"applyStyles",enabled:!1}]),{...e,...typeof this._config.popperConfig=="function"?this._config.popperConfig(e):this._config.popperConfig}}_selectMenuItem({key:e,target:n}){const s=t.find(_o,this._menu).filter(e=>H(e));if(!s.length)return;Le(s,n,e===gt,!s.includes(n)).focus()}static jQueryInterface(e){return this.each(function(){const t=h.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}static clearMenus(e){if(e.button===to||e.type==="keyup"&&e.key!==vt)return;const n=t.find(vo);for(const a of n){const t=h.getInstance(a);if(!t||t._config.autoClose===!1)continue;const s=e.composedPath(),o=s.includes(t._menu);if(s.includes(t._element)||t._config.autoClose==="inside"&&!o||t._config.autoClose==="outside"&&o)continue;if(t._menu.contains(e.target)&&(e.type==="keyup"&&e.key===vt||/input|select|option|textarea|form/i.test(e.target.tagName)))continue;const i={relatedTarget:t._element};e.type==="click"&&(i.clickEvent=e),t._completeHide(i)}}static dataApiKeydownHandler(e){const a=/input|textarea/i.test(e.target.tagName),s=e.key===Qs,o=[Js,gt].includes(e.key);if(!o&&!s)return;if(a&&!s)return;e.preventDefault();const i=this.matches(S)?this:t.prev(this,S)[0]||t.next(this,S)[0]||t.findOne(S,e.delegateTarget.parentNode),n=h.getOrCreateInstance(i);if(o){e.stopPropagation(),n.show(),n._selectMenuItem(e);return}n._isShown()&&(e.stopPropagation(),n.hide(),i.focus())}}e.on(document,jt,S,h.dataApiKeydownHandler),e.on(document,jt,be,h.dataApiKeydownHandler),e.on(document,bt,h.clearMenus),e.on(document,co,h.clearMenus),e.on(document,bt,S,function(e){e.preventDefault(),h.getOrCreateInstance(this).toggle()}),c(h);const wt=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ot=".sticky-top",me="padding-right",xt="margin-right";class tt{constructor(){this._element=document.body}getWidth(){const e=document.documentElement.clientWidth;return Math.abs(window.innerWidth-e)}hide(){const e=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,me,t=>t+e),this._setElementAttributes(wt,me,t=>t+e),this._setElementAttributes(Ot,xt,t=>t-e)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,me),this._resetElementAttributes(wt,me),this._resetElementAttributes(Ot,xt)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(e,t,n){const s=this.getWidth(),o=e=>{if(e!==this._element&&window.innerWidth>e.clientWidth+s)return;this._saveInitialAttribute(e,t);const o=window.getComputedStyle(e).getPropertyValue(t);e.style.setProperty(t,`${n(Number.parseFloat(o))}px`)};this._applyManipulationCallback(e,o)}_saveInitialAttribute(e,t){const n=e.style.getPropertyValue(t);n&&b.setDataAttribute(e,t,n)}_resetElementAttributes(e,t){const n=e=>{const n=b.getDataAttribute(e,t);if(n===null){e.style.removeProperty(t);return}b.removeDataAttribute(e,t),e.style.setProperty(t,n)};this._applyManipulationCallback(e,n)}_applyManipulationCallback(e,n){if(g(e)){n(e);return}for(const s of t.find(e,this._element))n(s)}}const Ft="backdrop",Ho="fade",pt="show",Dt=`mousedown.bs.${Ft}`,Vo={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},$o={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Nt extends te{constructor(e){super(),this._config=this._getConfig(e),this._isAppended=!1,this._element=null}static get Default(){return Vo}static get DefaultType(){return $o}static get NAME(){return Ft}show(e){if(!this._config.isVisible){y(e);return}this._append();const t=this._getElement();this._config.isAnimated&&ne(t),t.classList.add(pt),this._emulateAnimation(()=>{y(e)})}hide(e){if(!this._config.isVisible){y(e);return}this._getElement().classList.remove(pt),this._emulateAnimation(()=>{this.dispose(),y(e)})}dispose(){if(!this._isAppended)return;e.off(this._element,Dt),this._element.remove(),this._isAppended=!1}_getElement(){if(!this._element){const e=document.createElement("div");e.className=this._config.className,this._config.isAnimated&&e.classList.add(Ho),this._element=e}return this._element}_configAfterMerge(e){return e.rootElement=E(e.rootElement),e}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),e.on(t,Dt,()=>{y(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(e){Yn(e,this._getElement(),this._config.isAnimated)}}const Uo="focustrap",Ko="bs.focustrap",_e=`.${Ko}`,Yo=`focusin${_e}`,Go=`keydown.tab${_e}`,Xo="Tab",Qo="forward",Rt="backward",Jo={autofocus:!0,trapElement:null},ei={autofocus:"boolean",trapElement:"element"};class Pt extends te{constructor(e){super(),this._config=this._getConfig(e),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Jo}static get DefaultType(){return ei}static get NAME(){return Uo}activate(){if(this._isActive)return;this._config.autofocus&&this._config.trapElement.focus(),e.off(document,_e),e.on(document,Yo,e=>this._handleFocusin(e)),e.on(document,Go,e=>this._handleKeydown(e)),this._isActive=!0}deactivate(){if(!this._isActive)return;this._isActive=!1,e.off(document,_e)}_handleFocusin(e){const{trapElement:n}=this._config;if(e.target===document||e.target===n||n.contains(e.target))return;const s=t.focusableChildren(n);s.length===0?n.focus():this._lastTabNavDirection===Rt?s[s.length-1].focus():s[0].focus()}_handleKeydown(e){if(e.key!==Xo)return;this._lastTabNavDirection=e.shiftKey?Rt:Qo}}const ni="modal",si="bs.modal",d=`.${si}`,ii=".data-api",ai="Escape",ri=`hide${d}`,ci=`hidePrevented${d}`,Ht=`hidden${d}`,It=`show${d}`,ui=`shown${d}`,hi=`resize${d}`,mi=`click.dismiss${d}`,fi=`mousedown.dismiss${d}`,pi=`keydown.dismiss${d}`,gi=`click${d}${ii}`,Bt="modal-open",bi="fade",Vt="show",Ne="modal-static",_i=".modal.show",wi=".modal-dialog",Oi=".modal-body",xi='[data-bs-toggle="modal"]',Ci={backdrop:!0,focus:!0,keyboard:!0},Ei={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class B extends u{constructor(e,n){super(e,n),this._dialog=t.findOne(wi,this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new tt,this._addEventListeners()}static get Default(){return Ci}static get DefaultType(){return Ei}static get NAME(){return ni}toggle(e){return this._isShown?this.hide():this.show(e)}show(t){if(this._isShown||this._isTransitioning)return;const n=e.trigger(this._element,It,{relatedTarget:t});if(n.defaultPrevented)return;this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Bt),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t))}hide(){if(!this._isShown||this._isTransitioning)return;const t=e.trigger(this._element,ri);if(t.defaultPrevented)return;this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Vt),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())}dispose(){for(const t of[window,this._dialog])e.off(t,d);this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Nt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Pt({trapElement:this._element})}_showElement(n){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const s=t.findOne(Oi,this._dialog);s&&(s.scrollTop=0),ne(this._element),this._element.classList.add(Vt);const o=()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,e.trigger(this._element,ui,{relatedTarget:n})};this._queueCallback(o,this._dialog,this._isAnimated())}_addEventListeners(){e.on(this._element,pi,e=>{if(e.key!==ai)return;if(this._config.keyboard){e.preventDefault(),this.hide();return}this._triggerBackdropTransition()}),e.on(window,hi,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),e.on(this._element,fi,t=>{e.one(this._element,mi,e=>{if(this._element!==t.target||this._element!==e.target)return;if(this._config.backdrop==="static"){this._triggerBackdropTransition();return}this._config.backdrop&&this.hide()})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Bt),this._resetAdjustments(),this._scrollBar.reset(),e.trigger(this._element,Ht)})}_isAnimated(){return this._element.classList.contains(bi)}_triggerBackdropTransition(){const n=e.trigger(this._element,ci);if(n.defaultPrevented)return;const s=this._element.scrollHeight>document.documentElement.clientHeight,t=this._element.style.overflowY;if(t==="hidden"||this._element.classList.contains(Ne))return;s||(this._element.style.overflowY="hidden"),this._element.classList.add(Ne),this._queueCallback(()=>{this._element.classList.remove(Ne),this._queueCallback(()=>{this._element.style.overflowY=t},this._dialog)},this._dialog),this._element.focus()}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),n=e>0;if(n&&!t){const t=a()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!n&&t){const t=a()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(e,t){return this.each(function(){const n=B.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof n[e]=="undefined")throw new TypeError(`No method named "${e}"`);n[e](t)})}}e.on(document,gi,xi,function(n){const s=v(this);["A","AREA"].includes(this.tagName)&&n.preventDefault(),e.one(s,It,t=>{if(t.defaultPrevented)return;e.one(s,Ht,()=>{H(this)&&this.focus()})});const o=t.findOne(_i);o&&B.getInstance(o).hide();const i=B.getOrCreateInstance(s);i.toggle(this)}),Se(B),c(B);const Ai="offcanvas",Si="bs.offcanvas",j=`.${Si}`,Ut=".data-api",Ti=`load${j}${Ut}`,zi="Escape",qt="show",Yt="showing",an="hiding",Ri="offcanvas-backdrop",rn=".offcanvas.show",Hi=`show${j}`,Ii=`shown${j}`,Bi=`hide${j}`,cn=`hidePrevented${j}`,ln=`hidden${j}`,Wi=`resize${j}`,Ui=`click${j}${Ut}`,Ki=`keydown.dismiss${j}`,qi='[data-bs-toggle="offcanvas"]',Yi={backdrop:!0,keyboard:!0,scroll:!1},Gi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class A extends u{constructor(e,t){super(e,t),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Yi}static get DefaultType(){return Gi}static get NAME(){return Ai}toggle(e){return this._isShown?this.hide():this.show(e)}show(t){if(this._isShown)return;const n=e.trigger(this._element,Hi,{relatedTarget:t});if(n.defaultPrevented)return;this._isShown=!0,this._backdrop.show(),this._config.scroll||(new tt).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Yt);const s=()=>{(!this._config.scroll||this._config.backdrop)&&this._focustrap.activate(),this._element.classList.add(qt),this._element.classList.remove(Yt),e.trigger(this._element,Ii,{relatedTarget:t})};this._queueCallback(s,this._element,!0)}hide(){if(!this._isShown)return;const t=e.trigger(this._element,Bi);if(t.defaultPrevented)return;this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(an),this._backdrop.hide();const n=()=>{this._element.classList.remove(qt,an),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new tt).reset(),e.trigger(this._element,ln)};this._queueCallback(n,this._element,!0)}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const n=()=>{if(this._config.backdrop==="static"){e.trigger(this._element,cn);return}this.hide()},t=Boolean(this._config.backdrop);return new Nt({className:Ri,isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?n:null})}_initializeFocusTrap(){return new Pt({trapElement:this._element})}_addEventListeners(){e.on(this._element,Ki,t=>{if(t.key!==zi)return;if(!this._config.keyboard){e.trigger(this._element,cn);return}this.hide()})}static jQueryInterface(e){return this.each(function(){const t=A.getOrCreateInstance(this,e);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e](this)})}}e.on(document,Ui,qi,function(n){const s=v(this);if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),w(this))return;e.one(s,ln,()=>{H(this)&&this.focus()});const o=t.findOne(rn);o&&o!==s&&A.getInstance(o).hide();const i=A.getOrCreateInstance(s);i.toggle(this)}),e.on(window,Ti,()=>{for(const e of t.find(rn))A.getOrCreateInstance(e).show()}),e.on(window,Wi,()=>{for(const e of t.find("[aria-modal][class*=show][class*=offcanvas-]"))getComputedStyle(e).position!=="fixed"&&A.getOrCreateInstance(e).hide()}),Se(A),c(A);const Qi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Zi=/^aria-[\w-]*$/i,Ji=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,ea=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,ta=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!Qi.has(n)||Boolean(Ji.test(e.nodeValue)||ea.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))},dn={"*":["class","dir","id","lang","role",Zi],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]};function sa(e,t,n){if(!e.length)return e;if(n&&typeof n=="function")return n(e);const o=new window.DOMParser,s=o.parseFromString(e,"text/html"),i=[].concat(...s.body.querySelectorAll("*"));for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[].concat(...e.attributes),o=[].concat(t["*"]||[],t[n]||[]);for(const t of s)ta(t,o)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const oa="TemplateFactory",ia={allowList:dn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
      "},aa={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ra={entry:"(string|element|function|null)",selector:"(string|element)"};class ca extends te{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return ia}static get DefaultType(){return aa}static get NAME(){return oa}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},ra)}_setContent(e,n,s){const o=t.findOne(s,e);if(!o)return;if(n=this._resolvePossibleFunction(n),!n){o.remove();return}if(g(n)){this._putElementInTemplate(E(n),o);return}if(this._config.html){o.innerHTML=this._maybeSanitize(n);return}o.textContent=n}_maybeSanitize(e){return this._config.sanitize?sa(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return typeof e=="function"?e(this):e}_putElementInTemplate(e,t){if(this._config.html){t.innerHTML="",t.append(e);return}t.textContent=e.textContent}}const la="tooltip",da=new Set(["sanitize","allowList","sanitizeFn"]),Xe="fade",ha="modal",ue="show",fa=".tooltip-inner",hn=`.${ha}`,mn="hide.bs.modal",J="hover",ot="focus",ja="click",ya="manual",_a="hide",wa="hidden",Oa="show",xa="shown",Ca="inserted",Ea="click",ka="focusin",Aa="focusout",Sa="mouseenter",Ma="mouseleave",Fa={AUTO:"auto",TOP:"top",RIGHT:a()?"left":"right",BOTTOM:"bottom",LEFT:a()?"right":"left"},Ta={allowList:dn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},za={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class U extends u{constructor(e,t){if(typeof _t=="undefined")throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Ta}static get DefaultType(){return za}static get NAME(){return la}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){if(!this._isEnabled)return;if(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()){this._leave();return}this._enter()}dispose(){clearTimeout(this._timeout),e.off(this._element.closest(hn),mn,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if(this._element.style.display==="none")throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const n=e.trigger(this._element,this.constructor.eventName(Oa)),s=Jn(this._element),o=(s||this._element.ownerDocument.documentElement).contains(this._element);if(n.defaultPrevented||!o)return;this._disposePopper();const t=this._getTipElement();this._element.setAttribute("aria-describedby",t.getAttribute("id"));const{container:i}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(i.append(t),e.trigger(this._element,this.constructor.eventName(Ca))),this._popper=this._createPopper(t),t.classList.add(ue),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))e.on(t,"mouseover",Oe);const a=()=>{e.trigger(this._element,this.constructor.eventName(xa)),this._isHovered===!1&&this._leave(),this._isHovered=!1};this._queueCallback(a,this.tip,this._isAnimated())}hide(){if(!this._isShown())return;const t=e.trigger(this._element,this.constructor.eventName(_a));if(t.defaultPrevented)return;const n=this._getTipElement();if(n.classList.remove(ue),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))e.off(t,"mouseover",Oe);this._activeTrigger[ja]=!1,this._activeTrigger[ot]=!1,this._activeTrigger[J]=!1,this._isHovered=null;const s=()=>{if(this._isWithActiveTrigger())return;this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),e.trigger(this._element,this.constructor.eventName(wa))};this._queueCallback(s,this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();if(!t)return null;t.classList.remove(Xe,ue),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=Gr(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add(Xe),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new ca({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[fa]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Xe)}_isShown(){return this.tip&&this.tip.classList.contains(ue)}_createPopper(e){const t=typeof this._config.placement=="function"?this._config.placement.call(this,e,this._element):this._config.placement,n=Fa[t.toUpperCase()];return he(this._element,e,this._getPopperConfig(n))}_getOffset(){const{offset:e}=this._config;return typeof e=="string"?e.split(",").map(e=>Number.parseInt(e,10)):typeof e=="function"?t=>e(t,this._element):e}_resolvePossibleFunction(e){return typeof e=="function"?e.call(this._element):e}_getPopperConfig(e){const t={placement:e,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:e=>{this._getTipElement().setAttribute("data-popper-placement",e.state.placement)}}]};return{...t,...typeof this._config.popperConfig=="function"?this._config.popperConfig(t):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const n of t)if(n==="click")e.on(this._element,this.constructor.eventName(Ea),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t.toggle()});else if(n!==ya){const t=n===J?this.constructor.eventName(Sa):this.constructor.eventName(ka),s=n===J?this.constructor.eventName(Ma):this.constructor.eventName(Aa);e.on(this._element,t,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[e.type==="focusin"?ot:J]=!0,t._enter()}),e.on(this._element,s,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger[e.type==="focusout"?ot:J]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},e.on(this._element.closest(hn),mn,this._hideModalHandler)}_fixTitle(){const e=this._element.getAttribute("title");if(!e)return;!this._element.getAttribute("aria-label")&&!this._element.textContent.trim()&&this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title")}_enter(){if(this._isShown()||this._isHovered){this._isHovered=!0;return}this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show)}_leave(){if(this._isWithActiveTrigger())return;this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide)}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=b.getDataAttributes(this._element);for(const e of Object.keys(t))da.has(e)&&delete t[e];return e={...t,...typeof e=="object"&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=e.container===!1?document.body:E(e.container),typeof e.delay=="number"&&(e.delay={show:e.delay,hide:e.delay}),typeof e.title=="number"&&(e.title=e.title.toString()),typeof e.content=="number"&&(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const t in this._config)this.constructor.Default[t]!==this._config[t]&&(e[t]=this._config[t]);return e.selector=!1,e.trigger="manual",e}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(e){return this.each(function(){const t=U.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}}c(U);const Na="popover",La=".popover-header",Ra=".popover-body",Pa={...U.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Ha={...U.DefaultType,content:"(null|string|element|function)"};class ct extends U{static get Default(){return Pa}static get DefaultType(){return Ha}static get NAME(){return Na}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[La]:this._getTitle(),[Ra]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(e){return this.each(function(){const t=ct.getOrCreateInstance(this,e);if(typeof e!="string")return;if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e]()})}}c(ct);const Ba="scrollspy",Va="bs.scrollspy",lt=`.${Va}`,Wa=".data-api",Ua=`activate${lt}`,pn=`click${lt}`,qa=`load${lt}${Wa}`,Ya="dropdown-item",q="active",Xa='[data-bs-spy="scroll"]',mt="[href]",Za=".nav, .list-group",gn=".nav-link",er=".nav-item",tr=".list-group-item",nr=`${gn}, ${er} > ${gn}, ${tr}`,sr=".dropdown",or=".dropdown-toggle",ir={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ar={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ee extends u{constructor(e,t){super(e,t),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=getComputedStyle(this._element).overflowY==="visible"?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ir}static get DefaultType(){return ar}static get NAME(){return Ba}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const e of this._observableSections.values())this._observer.observe(e)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(e){return e.target=E(e.target)||document.body,e.rootMargin=e.offset?`${e.offset}px 0px -30%`:e.rootMargin,typeof e.threshold=="string"&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_maybeEnableSmoothScroll(){if(!this._config.smoothScroll)return;e.off(this._config.target,pn),e.on(this._config.target,pn,mt,e=>{const t=this._observableSections.get(e.target.hash);if(t){e.preventDefault();const n=this._rootElement||window,s=t.offsetTop-this._element.offsetTop;if(n.scrollTo){n.scrollTo({top:s,behavior:"smooth"});return}n.scrollTop=s}})}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(e=>this._observerCallback(e),e)}_observerCallback(e){const n=e=>this._targetLinks.get(`#${e.target.id}`),s=e=>{this._previousScrollData.visibleEntryTop=e.target.offsetTop,this._process(n(e))},t=(this._rootElement||document.documentElement).scrollTop,o=t>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=t;for(const i of e){if(!i.isIntersecting){this._activeTarget=null,this._clearActiveClass(n(i));continue}const a=i.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(o&&a){if(s(i),!t)return;continue}!o&&!a&&s(i)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const e=t.find(mt,this._config.target);for(const n of e){if(!n.hash||w(n))continue;const s=t.findOne(n.hash,this._element);H(s)&&(this._targetLinks.set(n.hash,n),this._observableSections.set(n.hash,s))}}_process(t){if(this._activeTarget===t)return;this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(q),this._activateParents(t),e.trigger(this._element,Ua,{relatedTarget:t})}_activateParents(e){if(e.classList.contains(Ya)){t.findOne(or,e.closest(sr)).classList.add(q);return}for(const n of t.parents(e,Za))for(const e of t.prev(n,nr))e.classList.add(q)}_clearActiveClass(e){e.classList.remove(q);const n=t.find(`${mt}.${q}`,e);for(const e of n)e.classList.remove(q)}static jQueryInterface(e){return this.each(function(){const t=Ee.getOrCreateInstance(this,e);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()})}}e.on(window,qa,()=>{for(const e of t.find(Xa))Ee.getOrCreateInstance(e)}),c(Ee);const cr="tab",lr="bs.tab",M=`.${lr}`,ur=`hide${M}`,hr=`hidden${M}`,mr=`show${M}`,fr=`shown${M}`,pr=`click${M}`,gr=`keydown${M}`,vr=`load${M}`,br="ArrowLeft",bn="ArrowRight",yr="ArrowUp",yn="ArrowDown",N="active",Mn="fade",We="show",Cr="dropdown",Er=".dropdown-toggle",kr=".dropdown-menu",$e=":not(.dropdown-toggle)",Sr='.list-group, .nav, [role="tablist"]',Mr=".nav-item, .list-group-item",Fr=`.nav-link${$e}, .list-group-item${$e}, [role="tab"]${$e}`,Kn='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',De=`${Fr}, ${Kn}`,Dr=`.${N}[data-bs-toggle="tab"], .${N}[data-bs-toggle="pill"], .${N}[data-bs-toggle="list"]`;class R extends u{constructor(t){if(super(t),this._parent=this._element.closest(Sr),!this._parent)return;this._setInitialAttributes(this._parent,this._getChildren()),e.on(this._element,gr,e=>this._keydown(e))}static get NAME(){return cr}show(){const t=this._element;if(this._elemIsActive(t))return;const n=this._getActiveElem(),s=n?e.trigger(n,ur,{relatedTarget:t}):null,o=e.trigger(t,mr,{relatedTarget:n});if(o.defaultPrevented||s&&s.defaultPrevented)return;this._deactivate(n,t),this._activate(t,n)}_activate(t,n){if(!t)return;t.classList.add(N),this._activate(v(t));const s=()=>{if(t.getAttribute("role")!=="tab"){t.classList.add(We);return}t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),e.trigger(t,fr,{relatedTarget:n})};this._queueCallback(s,t,t.classList.contains(Mn))}_deactivate(t,n){if(!t)return;t.classList.remove(N),t.blur(),this._deactivate(v(t));const s=()=>{if(t.getAttribute("role")!=="tab"){t.classList.remove(We);return}t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),e.trigger(t,hr,{relatedTarget:n})};this._queueCallback(s,t,t.classList.contains(Mn))}_keydown(e){if(![br,bn,yr,yn].includes(e.key))return;e.stopPropagation(),e.preventDefault();const n=[bn,yn].includes(e.key),t=Le(this._getChildren().filter(e=>!w(e)),e.target,n,!0);t&&(t.focus({preventScroll:!0}),R.getOrCreateInstance(t).show())}_getChildren(){return t.find(De,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=v(e);if(!t)return;this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`#${e.id}`)}_toggleDropDown(e,n){const s=this._getOuterElement(e);if(!s.classList.contains(Cr))return;const o=(e,o)=>{const i=t.findOne(e,s);i&&i.classList.toggle(o,n)};o(Er,N),o(kr,We),s.setAttribute("aria-expanded",n)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains(N)}_getInnerElement(e){return e.matches(De)?e:t.findOne(De,e)}_getOuterElement(e){return e.closest(Mr)||e}static jQueryInterface(e){return this.each(function(){const t=R.getOrCreateInstance(this);if(typeof e!="string")return;if(t[e]===void 0||e.startsWith("_")||e==="constructor")throw new TypeError(`No method named "${e}"`);t[e]()})}}e.on(document,pr,Kn,function(e){if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),w(this))return;R.getOrCreateInstance(this).show()}),e.on(window,vr,()=>{for(const e of t.find(Dr))R.getOrCreateInstance(e)}),c(R);const Lr="toast",Rr="bs.toast",C=`.${Rr}`,Hr=`mouseover${C}`,Ir=`mouseout${C}`,Br=`focusin${C}`,Vr=`focusout${C}`,$r=`hide${C}`,Wr=`hidden${C}`,Ur=`show${C}`,Kr=`shown${C}`,qr="fade",ts="hide",ge="show",we="showing",Qr={animation:"boolean",autohide:"boolean",delay:"number"},Zr={animation:!0,autohide:!0,delay:5e3};class Ce extends u{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Zr}static get DefaultType(){return Qr}static get NAME(){return Lr}show(){const t=e.trigger(this._element,Ur);if(t.defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add(qr);const n=()=>{this._element.classList.remove(we),e.trigger(this._element,Kr),this._maybeScheduleHide()};this._element.classList.remove(ts),ne(this._element),this._element.classList.add(ge,we),this._queueCallback(n,this._element,this._config.animation)}hide(){if(!this.isShown())return;const t=e.trigger(this._element,$r);if(t.defaultPrevented)return;const n=()=>{this._element.classList.add(ts),this._element.classList.remove(we,ge),e.trigger(this._element,Wr)};this._element.classList.add(we),this._queueCallback(n,this._element,this._config.animation)}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ge),super.dispose()}isShown(){return this._element.classList.contains(ge)}_maybeScheduleHide(){if(!this._config.autohide)return;if(this._hasMouseInteraction||this._hasKeyboardInteraction)return;this._timeout=setTimeout(()=>{this.hide()},this._config.delay)}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":{this._hasMouseInteraction=t;break}case"focusin":case"focusout":{this._hasKeyboardInteraction=t;break}}if(t){this._clearTimeout();return}const n=e.relatedTarget;if(this._element===n||this._element.contains(n))return;this._maybeScheduleHide()}_setListeners(){e.on(this._element,Hr,e=>this._onInteraction(e,!0)),e.on(this._element,Ir,e=>this._onInteraction(e,!1)),e.on(this._element,Br,e=>this._onInteraction(e,!0)),e.on(this._element,Vr,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(e){return this.each(function(){const t=Ce.getOrCreateInstance(this,e);if(typeof e=="string"){if(typeof t[e]=="undefined")throw new TypeError(`No method named "${e}"`);t[e](this)}})}}Se(Ce),c(Ce);const ec={Alert:de,Button:fe,Carousel:ie,Collapse:le,Dropdown:h,Modal:B,Offcanvas:A,Popover:ct,ScrollSpy:Ee,Tab:R,Toast:Ce,Tooltip:U};return ec}),function(e){"use strict";e(function(){e('[data-bs-toggle="tooltip"]').tooltip(),e('[data-bs-toggle="popover"]').popover(),e(".popover-dismiss").popover({trigger:"focus"})});function t(e){return e.offset().top+e.outerHeight()}e(function(){var n,o,i,s=e(".js-td-cover");if(!s.length)return;o=t(s),i=e(".js-navbar-scroll").offset().top,n=Math.ceil(e(".js-navbar-scroll").outerHeight()),o-i',t.href="#"+e.id,e.insertAdjacentElement("beforeend",t),e.addEventListener("mouseenter",function(){t.style.visibility="initial"}),e.addEventListener("mouseleave",function(){t.style.visibility="hidden"})}})})}(jQuery),function(e){"use strict";var t={init:function(){e(document).ready(function(){e(document).on("keypress",".td-search input",function(t){if(t.keyCode!==13)return;var n=e(this).val(),s="search/?q="+n;return document.location=s,!1})})}};t.init()}(jQuery) \ No newline at end of file diff --git a/docs/public/no/404.html b/docs/public/no/404.html index 4ccc635d1..052390ab3 100644 --- a/docs/public/no/404.html +++ b/docs/public/no/404.html @@ -1,7 +1,162 @@ -404 Page not found | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +404 Page not found | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      Not found

      Oops! This page doesn't exist. Try going back to the home page.

      You can learn how to make a 404 page like this in Custom 404 Pages.

      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      +

      Not found

      +

      Oops! This page doesn't exist. Try going back to the home page.

      +

      You can learn how to make a 404 page like this in Custom 404 Pages.

      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/no/categories/index.html b/docs/public/no/categories/index.html index 6e1cf0c09..1f8c9cbcf 100644 --- a/docs/public/no/categories/index.html +++ b/docs/public/no/categories/index.html @@ -1,7 +1,163 @@ -Categories | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Categories | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      Categories

      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      +
      +

      Categories

      +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/no/categories/index.xml b/docs/public/no/categories/index.xml index 335d9299b..af9f6b744 100644 --- a/docs/public/no/categories/index.xml +++ b/docs/public/no/categories/index.xml @@ -1 +1,18 @@ -SOFAServerless – Categories/no/categories/Recent content in Categories on SOFAServerlessHugo -- gohugo.iono \ No newline at end of file + + + SOFAServerless – Categories + /no/categories/ + Recent content in Categories on SOFAServerless + Hugo -- gohugo.io + no + + + + + + + + + + + diff --git a/docs/public/no/index.html b/docs/public/no/index.html index 19bbd6de2..142989650 100644 --- a/docs/public/no/index.html +++ b/docs/public/no/index.html @@ -1,7 +1,161 @@ -SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
      - - \ No newline at end of file + + + +
      + +
      +
      +
      + + + +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/no/index.xml b/docs/public/no/index.xml index 03efb340a..1c67636b2 100644 --- a/docs/public/no/index.xml +++ b/docs/public/no/index.xml @@ -1 +1,17 @@ -SOFAServerless – SOFAServerless/no/Recent content on SOFAServerlessHugo -- gohugo.iono \ No newline at end of file + + + SOFAServerless – SOFAServerless + /no/ + Recent content on SOFAServerless + Hugo -- gohugo.io + no + + + + + + + + + + diff --git a/docs/public/no/sitemap.xml b/docs/public/no/sitemap.xml index 03549e6d3..3e4b45167 100644 --- a/docs/public/no/sitemap.xml +++ b/docs/public/no/sitemap.xml @@ -1 +1,41 @@ -/no/categories//no//no/tags/ \ No newline at end of file + + + + /no/categories/ + + + + /no/ + + + + /no/tags/ + + + + diff --git a/docs/public/no/tags/index.html b/docs/public/no/tags/index.html index d5c644b8e..19d120ad3 100644 --- a/docs/public/no/tags/index.html +++ b/docs/public/no/tags/index.html @@ -1,7 +1,163 @@ -Tags | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Tags | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      Tags

      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      +
      +

      Tags

      +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/no/tags/index.xml b/docs/public/no/tags/index.xml index d52f1d4b3..1d72ff23d 100644 --- a/docs/public/no/tags/index.xml +++ b/docs/public/no/tags/index.xml @@ -1 +1,18 @@ -SOFAServerless – Tags/no/tags/Recent content in Tags on SOFAServerlessHugo -- gohugo.iono \ No newline at end of file + + + SOFAServerless – Tags + /no/tags/ + Recent content in Tags on SOFAServerless + Hugo -- gohugo.io + no + + + + + + + + + + + diff --git a/docs/public/search/fragment/zh-cn_10a5166.pf_fragment b/docs/public/search/fragment/zh-cn_10a5166.pf_fragment deleted file mode 100644 index 63b0edaa086493a3748e4b9f6140981b62f900ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 813 zcmV+|1Je8-iwFP!00002|BX~zPZL2D{wumqnsnV>KuTYXG4WA-&=_M3nVmUpH%@o! z?ks8uNg>{-w-Oa^HCTiMqZBbHP!Rr$vdi-1UvSQ>Ekz*muruGBZ@zQR&Y3x7Qh>Z= zvjIlgkmkEw%V`F4sAo{e7OrLb!g6fA;9Cs1WrJsw9OiS-742{iZrj4GJkZ;egP|of zq`?-_TR1thRe$)i@^HKHZTsDn1YxVT^?u}tL|bbU(fT-==BDEvXbqQEgX((;g1K5p zr--Mu%@%G9C+>%h56Hc2Zcn{Z+>6DiI*MTIS@8Ia1Sgbvwh_F0iJ_iP2ID`1@!7CG zc_^|+e$hf>`*lr4ej06{)6LesUBWka_R4>+y+H>TTqqM4B>Ghuk>jOu!6nSH3mwZD zr&Q)3fORaV2Z_ar$zgAb)$d_tB&@gE%vk_t5_-M8zDzuvMA1@nSr!)OBJgtF=MtU5 z=z_SX~I`#tw|~wb30#DlxIqN;JRJ*%Jrr$o~lzKA4CqcQKo{ zi{X6l<(s+bxNLwrK0cdN zGM&)!78l@pT5f1Kp_M=oi8Nse4*0Ir&FH>*(uGx$WtU`8h_aV6jDjVKzIlQ=CBt*_ z#1rzuD5QJf4uZ?U^9Fde#WHUzUB+$NWGOGhI`}Ll5e?~X^X z%d)-gSObLPBah?CEQw8v4yJr{NCOU~xCbz65Cu#^!E{gd5|g;PBz{*({_v4^UdjHx00sa6%gmDh diff --git a/docs/public/search/fragment/zh-cn_129e997.pf_fragment b/docs/public/search/fragment/zh-cn_129e997.pf_fragment deleted file mode 100644 index 19c7b837a3d5dfdc2023415b596aaddfbd72ac81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 805 zcmV+=1KRu_iwFP!00002|BX~zPZ~iK{wsE$+60Qy)EHhG)5J6}y(F46P1BfVchHRs zOZH-$7!ssd1utM~(O67H#cEX$OCz!%|D|PimnZ*1=gc5tqe&lj=G$|b^PO{MPM9A@ z0VyPoiGn!I8LG_rIFG0ZnnyQuMU{A2^8^*qkda#nBbC=BCFJ4NNg3&GP3J=*FDoJB zHZ*dMhjVd)64FsfC#P87@^*INcXXkZ`_mC5f+Z@Lx|q2H|*7Y z5Dik+R|f2_hgxKtl`5c-mqUXiNS#8ej5Lj&zMsRJYd|Trf-4^ha2mkPHp{SX#OYDv zBxAB)*5<*ou+gk9fwp8eYc((!9vJDlHXkrW%v0w@Ji8BJ;w5{} z1oP*#`cFw+RYjJOdP{Z${E5b>J5aPX%B}1vXse%Ktwe1zPPGYVXBNOx#i`f3(`!?E zHahx>roEiUsU6A?=rzq{CwhaYQ#iQ2V7PeR_MeAdSTUN++z)(KVqxy8B|iU&QzxB} z9R{72JHD$@6Z8ar-yo`e zp-|Qr(7D#(I-V_e$82Xqld!T0kW>mdc|gEDTBoCJr?E_HT!l<_FYscOU0AR+t4^VC zO_W8kk|panac7j%Tz3@K)>bU)6wf*~ELg|=pJ3K}#)(3g?XP80#rCDj#Aylh-HvTq zRyK;$G>JyRT0N(IMPVHU{p_-Ny<~Aw)a!jjHXmSK6k&r=0sDROp@0zR_do6@@BuV~ zjDz=zDvk+?0TZFmJK*91lB^?D<9yQ*7Z*f2PY|a|x{S!kVAs0+V%Y=bh#4OeCKT{_ zJI#@o6J+y6l%_}kNhDAg-I^pKKd-up(ja0TeE=JRWJRDui}UqHT;2X5{)Rsiz{<^} j|KZMGIfqq=2s%D&9oG9pZb%|>R)$t z+WkHK*aM(~)@1Y91wfNO?P&ZOLxZh^fl;-+7kZlj!a-}MixaO}2LTdQ`-SR$#@u^< z+S`b8??#c=K+t>>zWf2;3WjgT9mNFCRh?m4d_KY*&RLOaOGo~FYNL^55!)&p652A? zt0hMVbPL@FJkH)VaSg{S*z32AaCwy8KOF9&+xUWOr-_jrKlHu=xEOczy%+7T131_T z@U*tZ;aZTK19JC*$oB#Ka~sE1l=o8&nrBw|)lF9cB{tS@qv7_9lN6iDPZd)u?Z{om zZa$3EdLL8%TG)OLAR6}5!o#*y#|>NYPwe~Z?JkDy@v7vNBDc`V=V8gjXfA&ZOaU=v z3=K{j+Dq{9l2)28UY`ee87~8-?1^cy1$}Abs!Fu diff --git a/docs/public/search/fragment/zh-cn_1626e9a.pf_fragment b/docs/public/search/fragment/zh-cn_1626e9a.pf_fragment deleted file mode 100644 index 235974cbc8c6fe14665a6408cd2e82034c1f63c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2474 zcmV;b303wViwFP!00002|D{;#PaD}4|5seAAG#9Q7znQ~RT`;MCAO9Jp{lB?XlxHK zi?J#=LMkWn{^tC$pd#fPbN-{umi!ynuSsQ12yJXXHwq_g+rIH5W3si@IIhCO z|FYD#W^EJMx;$c$m$;KJJ{TKix}UkvEd58e-F$XSU!CTASq*YobT>=QEENgrHr7tt zMmE1oPL?pH0Uizd^K~+%#Yfpfu}l_OgceqWYiu_48!DiFTLS^qxax*kIwaW}7jJOC zadyDi%{ER}xGIlmzEA2tbS?dXl@V<;lTzqmvEw)Ui4+U>YG>Bw8>dT5(z#8y{hSof zoD}uSQoZ_#QEZzB%d(#Ib0RewNVCl&DP=oYU~~{VTZ;#bqORFDIU|6%O4umRabz(f6O^^LYl~0@AHW& zkn;`PgSdjxZeauYM&CzOeYizYmiD$KGzLmiiHxbj+brRz&%adS0JJGawvgrd5Q!=2 z^l(Cn@(;{#vv5gP*SVu@f#O)AJo>O#tY5vODt@Rs*gk5<7T~lU+xAGt!BR>B;jH=< z!+_HtNomW5ob7$UQm3P{^~$zTj;xmJRc2l3u5nvJ?4|1`YsSI`gSt%_*q|hsx3@Nw zE@Y8h@aHkMj**2ZpGbDo)@rrP;b*Caz{!K@5lI_X88;W2Vn)#2TKZuur7F>>KZ&f5 zGHBZVXcU-{wKcN+x(y&(+H*Z@I1*SWz!;7>1)uXHIplB-jr=7wKeqyIgvyARfUh=! zlH(cU^$wPvwg-kQmfy5bd2O5Ijwo?DlT=gEiH4;P$B}TA%K8;6=g&?_ehz`kc$Q-_ zwtOO=MeP!nx=g@RckuH&Z-$HEgSiwtQ$rXwGTLabFWv#;Q>9k6PYB?17y=3U#aOtCKXY? z)wNhU09f_ySHlkeBHnSAy<&VBfEm-cde*1#u`RhjE!rg=}=ZvK@JA=7GWNwpp0oAu~EpX~|L3Xor_iy3v z3H*gK@n}pPQtVb`$Wn7ei>ZO6qL0D0>CZOBIwpT$jL{%VMZ1i*+vU5rz(Q$dNzWT3 zN#Q^;l>ju=H8m|@v|Pm#1Mb2r68EQ_8^ zI8M%-x^xx|hWXQF8>q0Vg>aLxxG1?v(;aT|xtNQFfjd{$i0gaqvev#k5*5v+a`@)p7T&BE77>j}+~}f{WHQud&aYb6&$7qX_i!Xj90=ocH1WC2+}5 zP)Kb_XOwDPq29@-!N-fz0Xnp^qsz$>~ zWZZiHtHKP{$29hvgex`u9aCe;kqm89e@|ckhHm+Df~Dpio9?~eAi0n7FcI2c`pSwK zwq4|^L>B4r>NiaFvo~g~ATFwn!#lEE!74`gRx4&nzHl)q#^ML#e8E!+-o-2_2n5ZU ztXaMkdwAz+k-LN$`E@g!lW&?7U)E=(Aqyw8CTTla96;4;B?tj}<$HA^jEzgE-)c&_ zySjVeXNVM+OF>{ev`Hzo6lOu`djnqK_kq44wS;_4Ic5p-0ix zRN6oEWV*vYq3Q}e`E^Z?sW2MmFar4kXd%W72o=Cgn}A@#wlQ5#riX$-4TjH3T-71f zC#M2%5lt#n(ArD{)8kVyB{d#o|GI)y^I$SQ8VvOg_V)J#Lw$pNgFRh?y@R2Fp3cDs zp~2n~o37*P zXOJrvF&*{kHVqEM5q2E(p>9w4-zedj`j?SMIJ{4LC=hz!WH8ok(TnLhqA&~tL@+s{ zdv@3Yx&PiVl)UDyxVhl{mA&;x-ZkfSZ3>ZBAF`ilZPgk80OefZ@Bjb+ diff --git a/docs/public/search/fragment/zh-cn_17b6c95.pf_fragment b/docs/public/search/fragment/zh-cn_17b6c95.pf_fragment deleted file mode 100644 index 5f65bfd1f2e736c4cded6bc80ebcdb2b8decb904..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22608 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 DLh>g% diff --git a/docs/public/search/fragment/zh-cn_1e91b9f.pf_fragment b/docs/public/search/fragment/zh-cn_1e91b9f.pf_fragment deleted file mode 100644 index 923ed9c9f10620a9030007d746a2ef8dd4198605..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52570 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%0Dph zc|DzQ&B!Ku2F*T)i=n?-(=YuQmEB}%!9V@yx^WW-(FR!ZtT1@bUka6~o#aK3deZ5Y zbP0M`JUl-vN+FXh{SA=_A(oW`ohfPu0%uh#1YAhh$ioLAcO_ya2CzzKA2-66(0+J{rz8IaD_j5wN8mx$#!Osi0Wc#oM7a|5%S zzcksHyKYT%+_DT<^H#zuPn^)sIA={Bb zQI*t6t$t{*c7UH9wV665$jZm8lAgy5hP8aiPgbXBM-RjK6=0&sP(&qIw3n*-fVmpl zC`zAc{4_2z4(mz?Oi)MW7ta#;i&8BrrypZySE;`Z2I@pDm~PU>4optEh6RzyN*$73 z(zx1J1t;9!h1I$C_X;6h?15Vf{e#XUh@zF+{(cnTG2Ei;d6N74O|9Ddq4-a=I--Bn z(F=ROuc~W<+HhW`% z``V#H(A&Eo{E8+DPsPC>7)-wRe?Aefe)ZNz8YRpGzYoNtd3V&Q)gie(%t=m z<*Q=gMx9H&gB-o?#AJqb7<4`{9sVf<|FbUsJIC9JlBB!CkW8vupvaTCVF>4VynrFZ z8pA&Fx`g_>ifP&ituq&a>#R74xPm22kxsd;0WM~~Z$KJzU3YDY z4yvB#0bMLS5j=yTrw=+Z=RO90ID0MLvsFwwtAJeM%?nS1tEf{h_TVDiHP8~mPraGq z4(HZ1{A1{Temk1M@=yfHK={`b*r*Ood1#QI3;Dbxd(sT^&uk&3TO94~TJIsDO%un> z8xVj}Et5S*%Sm-ym$D&nls6k=bQD4#NpK#| z$&!z$)(sE-hOU4RL4u;@=Q0J6e3$Y6vj=u-4VWT+tM^!-S;Y$G6ueq%Qwk*Dsg1J^ zg>%T^f3NotocTawA^>k_}Y|JxEDl?|& zm+$mQ?dnHZc7@YLg_Y=~`;VySd+)7}_oJ%jo%i3y3zZW7@57eT5cqkIoeIi~6_5|> zVNu6_j9eT=mi{S*{e`gBH*_TrXW@UezcQ??zEjYqVq#ULZe6P~OZ|uJcyjd)FL_$9h(K6ai>fA0f;DSpC3U*&T$7Gw|cjNm2 zfqtu3a-tvimwE?83(?VgrCu?k9+b<%=&Xw=f3*B&9d^wKO-u_Ibm~V~zA22G2lchY zD-;EfSL?28;i%eoF^N9eczuQR|oM_WA2tC zR3=Edtay7De5N7`d-bs=G%?^l0Pmc=Ng#KtYLZaxE@z63Z|;Cjc>D6g_T4L-L#jt* zYz&=Vn2__Km|j<{zbAkWkZ2_GZ48(7v)FF+M-d12XS-NID6_a5P_XR?gT=?emWjv? z;#U80=ofokc3`%9TX!58U@2RosBiG|p9SUGcRqOQb1Bbrj*B!f?9fS)pe_qX>q1AH zxJzz~p-PIbTE@SA8_Xr<<|ThXT@teO;67v~D0kOXYE#hO%NhQ=psR-LC;{L4^}60i zyWwuPu)@EvcW$@P4lt5o-?>7I(}%c72Cd2e%$-}@mvDEzTLUgrr;MTeDT9aKD-D(p zi#^jqg?J{EJH7H}_feZZNVZeJNw*vbl|NqVQ6t6(81>M^m_a;sm|3nibvPy!KIyhG z+@+?wQn@?}OwKi6uQB!=%mW`94+1pdst?u0x&Qrl-hS)g$I18J1_c5Yr$XOq(V%1x48!OTy57*sG|W^88)XjV$O|~IZ|pz# z)+cZ7fA^C&e)s;H@4ofP`}^N}3oV=>lM^80_MrE8Q4F;H|HxVK3<(WsR2`!K&bo<8D1{4dO9e1*nfDhNI(D_Z(S$@C$;9Tnjn#c6RLeGs-6q5OE zUwP43cy3-%aVN?Xpz9lNtzO~{j9=_HOX{POGxI91bZGRaKd-Yue!4OnbHmNman==9 zJ>Y$r%FVhjEE?+;eGVq<5cb8u1Wj$)3s|lh^_{n7f!|>{OCALkdZaHjc@kz$2s$MH zID5Kz%kq_IOnlXtnW-};BEQ_Je;s}hB;oOH$wyvI46IJVXS>Xa$@Bj`=S zoCMI-G}O<86yE#l+Jj9QRsZ!pa0H(Mjd6$<)?`qdy?F(86@072HYV!YUyD+>Y^}_T z^%~>}`2K}rv8k{Qc4s`5ELAv3Pyli8>L9;UD&|Z*BI28l45m`4PB<&U<4D!7{q^%W zq2KUqMowj9x%OfB15<<{QJ9@Ht~ZR<_zTP6PAfJUBuNzePJFSoJWloxqg~g)fz6Cs z7EWgIzChq$8=`zB`$nJ#p#l%R;YVz2j5qF7H@I@FG{oGvjw_w^sRgZ_6`}h@EbF|Jq{G`c&ta~ z9>R-|E*IJ9n{aKMAQ&)!5iF7bzN01m70y+4;2cwdT!UA`S!%o2^Mqo;YcuCVs3TOw$#fhpkz#R0tdm*Y4J`LOgnB|FIn$3%o zItt^O-G>TfI$VR8ymTh3{8WrNl$>F;>%B?Rroud}TURAZHiO{iD!2j|=fU(XE<_(( zzbQ4;oID-9Q}VyYo|9>vQ4YXP}N5mx4e+ zsdZkUOWg{>DUxjAIzB915n^eb#RH*1f9a9n+(4bn_gLeUp4&lEGKF-m;~j7adG72Y zs+hcbWKA#=iV<1@wl#hZLGL}|dMjVlhZQh&{x}+~K77Nn_w%Cx!NBh&3}DtSf@?Fy zNm!_EcqfH0VV)>Kt@=TgA*J8-@pHzp<1s8R4(@vqm@&9`#Po-v)q)gkSL!;QnxP|(bQQH%JmS)y9}|0~_|P|ht8uBIg=j?)I`l6>HG3CrnCeFV zqBRkjWEPT1G{!IKyJM&B#9Y$K3F#Mh=UF~6v+i?Na&w9%Vo`J^9vl2yWPW{x)q*1V z^QY=gTK>QyvdN;gh}>=#@!tQ|Z{No&XMap{<4POYRl5H`A(lMS^Lf(d+$uG6XnUQr zkkFdN$0vD~LPs3NXo%dAj8SQO);(l~Ugzr7)uvy$XLX^eBx>kHt|8r`l9UVF#vvpj_+I z0l188=r8ECZvQ_N%{^}ipHh4U_jneqTn;<953hO#Lr>p8cUSI+hIC(5KTh*QzqNG1 zI+*EXN6eWTto4*jbecIK16RL7;7QSEZuw+7 z^OuEXrGH4N=+SWVEiHsX7XCWZ>l0dyL;W7i5e{W6vgM?(Qi$ew zVa>JPL$wNFZ%$Cl^j~&ahpU~!Vha8NNgbaQxv0FHV19>19m$ks{kmPT{@>fN6TE8g zV1KC$Mt?PQDsE+|WHu8?9Q0#P7>b30aKJ0|mk$S6!LV*S1i&&o!K*aTcWiK%*J&}C z)h!#}tU?M5W7blN0`>BP-~Jksl_gC8P7LUUIn)up0Q%Zo^U^{t^6(M{EhY9fuiS5~ zNHZ1Loz<|_T2kiJ?-v}ZH}IN$@1|q7Bb~w&cX5Cd-6kbv5CZJcSq0i9BR= zRu(UvBFJ%t=*ZLU3o52g*_-WZdM3c*!Y!fN^v^zQ%-v!dL&2+n3Z*f(;*Y;zZv-R( zfQ`%d{LRZShkQ`0$0LGpreZ;1+P~Ayl}+^roSif9Ga25BtNngkIH*wL`e<`w-ssEh zoG~VB?oCy@Lwh40_(-iryT(xv5Lvi`W1}s6!x$|T!)7W1Nu>i72kA=0$knC-h`C3w zkZGOgY0?s{Ds~$BVTD%p$I~AkZmFKTaC2}^Wr+{6AB<~TJ4Y?gwQBFlpz(Bk&F!6L{F*ySUMBje zje`GBhg7Tb0{;j3U`SO_Q13!gfd*83k_Q2Ivt*4PmbFQLKvq>aT&ha+J~q@_9$*M= z0T>P9nobD>gZxRms*8}RN7O|C4>qOFrPGi>&I$bD4+{Qi&^tuC{&xw^Jpx^q9029J z3JY6pKDyMrsFb!teTVu>74O5E7?S$LG>{)3_Ij;k(!HKnQU2@wJ-VqZD4_Zo@ZRvu zdG6AsGCHL*;DOpu@1V7u0l|t2V9Lx)dt6WJ@kxK>zO@F^nZ1mIW#xGoMT1P<+ea!G zSh?RBwtV=YeuW1a4x8^hyk;bY}Nl+U@PD^WQ0# zc4n7cTE*-328r_tyEFI7CPnqLH;!m_3QiATc~}$tS~Cjlf*=d+E3-{Mp3uMY36;ayL zd?IFmu;TS|kNq3NhG2s;8g;XTX?x7I_TO7pjuUo285Gj)CqugwcSS$PKCbpinfEi# z>wQB?xd%OQ>?$;s)ORWtN6EMYM#la29PN%A>EAHZ8O_@5%yb4BBv%@Q`Y?^vYnqb; z-t50JOBuyOGDx`KlB{{wSiU0crr=;@{F80Q-HHiIT4XyUExgmSe(nSiVr(Ua2=1SN_4`z}qu<|z;7vVpCf;keb=rQBRZim0va4=Z8xa_Z7 zh-OdY5hoG6fI@-kTt$Hil4;bOW;~z{qpUaDAjWuz@WLQ3A?AdAn{Rcb98O#D)OB51 zTHrZkeKGFdfe%g8h7K-ooB_H0QWP1% zz5{nHI1Lkaz0W#v-hju=viLHxYpcGzxk5qIyt3%e-Uq21&rPQXhdfj}x0{xzI#AUV zn_qt|PnKR?U8l9T2<$tmD7JrG<@PxK7;av?$tL~`yMs7{O-G^pz&SB;oiY^t%z`_C zNGh~`o*)<}IYWWrD2kDA-?i>M1X>ieV2cHz)~Gp%22wJ}_AgOatcG=xdaxcLV{7YH&|P7X-j%TM*{gXZd^ zZ?P{yIPJVIp|DMI0|ae~hAk$DjX1lA+L{gEOCIR&n(0Lpc z5GZ2^V7w{0sryfo{llgT)Bw5#LP-Y+bX40HH<^0p;?}xKD*NCZJxp29ids|iD^z|3 zC(FNrF5lm|%LIpzY7cXQ6COVZT&6asPK44J>sIPKF9vc2AJ!^CO{w9#-<3)RlYS#k zm(f%D962>dss`F$AHHEOXPv-EU=@-+oQF_t0)|-k>%v6qixG}Fh!6T`12v#v4!S<^ z1g7j;LZ=&k23HJzOze&Br7>~7aq$ZdS*!(od(@w~&7W)jE${(aizzopIPnO|FG*L% zN|?o1FEqPqn*oVkOBo<4HW9W7 zAw9Q(Ipvm4`_rp@L$$A50HogwP;!zQs*NUrtP;=RxXIJ1Pwxx_Fp zx9soj3n^Y^LpzXr3F8&IIkJ6c6>zAv2-=>f;`G}?Ifg0@a-H7Q6yOR+k-^g7kh0z& zFrHk*$)CQreL)qb+Pu>^&N4|sIY)4a16Y1$8c{p|A(=ADn@_&%)BX69eje z%1f5;-H!o>lSL=E`!Smy)w8(^-2(24y4*e0TDXH@%-K|;9`2vgPG>J9etm!cr$Py; z2^0JR&xScfpP2M~QPf*?bqJ0dn(c))9^{@Ut6OVpY&Wp^Z-sslCSv>-3^y*T$jD(% zJ!#%wHOPO!vY$DtGmu?S3ixZ!w&%Vf1)SJmRGbjD1+GGz+OI$USgAgQmeZeMg}%wV zLC;g@UBUr|b$-S7z=0|9Pg$C^b}dX$IT9M_peg>8ICNW8YbvM3QI zTu77_Q|BjlDzTf$G7T>Gf}NdeK6q@Xo90)Nj`H{rpqH&|@6{Q*xQ_Z?^xN*8Y}(TP ziEWt?S^boT#@Dg)zFLJprv7Ux-Gegf5?|Xr)T46v%5Sl-!E4BPYHhquOebq09Q=6y z$Gp_HD4}#RVBQ%h_g6c;9}>QWqrqQc zrn|xg5;Mb=LmD08ft~3H|7U^~3YQO2>La zg*Z2nscIL&FX_x4!(Y;`5T7#;pf>6QPnBl0E}**OpVe3Er;!V(f|~1Ksb4UdABMd7 zc2PNDrg=yPwiEe`{5-@JpuUs&U$%QI6f$6k(q&LhZ8p1s23@T`VDLnkK|lI7!Atn7 zMUcUaqW~5GXwT=fl(X(*4tq@OSE2hHz0?*qw=Xa3wP&Ha0gs*J--xsKC;D9P(d#?N zPdN8HvMJ8C2CwF$fs2=MHtv0^?hskeP>=(0E_?d)3ssmclgeZ@c}n9SDf#5%R1xWE zK7)T`GXGxJ)iQe{F9e6^F-D2=sqi7>PY2Qml-XG+%(0N}NTo748XR(NzNS4gx(azO z*IvK@+w-1YQ7&E3YwFCU^cLp|#eA`pYA;s0^6tG%KKba(<^m<++XVIs+MiSa{#&ky1C1CeQyf@cUL`o)OS8Snq#kzInJbl=BbUMMPhtj{ygV66!f=7~kEl`3zPx+Nb;L&<#cf6~Gg zD;uI@dL}O!k4}@`NmVN%D~0wpOj^w;I76rvL2aMhDY=mzfHnLGIS6m3hP={hOtZ+> zAJx#Y+kX1S{sY0MNSyYU$-Ejil&$+DaM?{Gpcv}bN!JGz%h3+BH-!1kYGLY872xNW zp<5G(XqrTKvDg)$d}bUx{h1RGs@0s|@F!RFB6vPZaX~jONcFfsiBU4m*-?M;Jt6Q$0gW^bfd$yL zq6_%|?d}PdDyzhw)`Sbiuui_;{4u2j(0O@V&+arQO+Uis?LT7fhL7j{kr#Dl zw4~s|=nu!aE*!zNa0O(3dh*V|05E#~^_%|u6g`)km|10CSUS0V{JiNws5PFb^7gtA z59K)GZhPP%-55W+wfePTMif$4JBNU5TkX{rH|AE*-*0>t)e{WgpogBGs^+zu7p@8G zw!yd$-#b7H|FFMyL>yjqpsZ5ux(9R=hr#@_SIps}F*)if4*s_v5ga}eWc|HTpMlrH z!`8wI{S?ap=%7o=Y0=Xnphe@(c-@$r4X_OQ>LUX?iq;J^m#vi*`ifWD(rOwk!~za0 zPBWqu;G0kFX}Q`zb{-rD{NW9M_G`01U8dBZf8Z~?FeHE!(HbEz3j5RypOy5sy?nn8 z7Wowwrm75^Flh76!q(=EI`gb+&93`%#vhEzchczolJc%o5w(rWw_zsgKyJW15%Nz? zh$EqfQ&Q5}33F67Phzbv1(_WPo{Jn<#Z@`>$k5(f%Ip35dh^MYvuW~=m01%FtZnhI z?X5FUpbmE9=|hm@2pF_h*h%!soE;T1U!4{Hq7#kEO9)O_dPF_@Gn1{wM|I<(rgv;Z zIihSz#UI3)0L-?}P0+vm`LC*jzaHvLXsR)g;CSy~8_fci=uBa2eN>&_CtE8DBiGMk z(K1aiww6{=3_O0VdFs9)-q+W`d7kb8#{)sYOk9@ef31mMredi zQxMd^w9Cvys&AXEl?VQLvK;>QBN*<|WB=|=5FL=?aER>yBM#5}m5Zp3=>4eF`ziX( zV8JM5*-**l(KM}gMsXB%j3ZYIc(&SlDIMqN8|7T&ev^aZiW4|l;7JUpqqrFKz16o2 z@(1jdg#K5zm83hmrP6a_KJ*95MQ~#SwgfEv>bPvJ9S6YT&c1vCE`D1p^Nr_9t)G>% z{Y8Se#l4l&R6>fsj&VYgE?+uj7!caiktbM*TYKr@@+U_cXTDDGyS2G+2a?1i^z2KS z$~sFVQ2871s=c-Bk0@U*h{eAuxWzGhJaPumsfRLpUE<@`f;xPQ8`=?XnqyF7;*39P zvdhzIERG6TQ*CDO&#EJ+&Y!kI8ebRkNNfx%r^eZv{;aYqO^?;J-z036o+D5Z6waV? z+J%gb+4+#&g?R-dkxYtwWe_2^?$+YP<_Ya;2K@b*LC<%Y; zk>G@0@J*oF!=-(Yv1l!^SpLX(YxBA}Y!}BHH|H8xHd<$m!_oHDsm9#bVzb3tP@?*y zPl-3<#Fh9EP^|frm#ncU98Wv-`eQTbZmDR@-s_sDgT&(IK{ifw!`3=bQ($NVv8 z%30epY~-asKSuo;^@Pqq6NwpVh8ay>oq7IR=m5bgwnn8CL{tYR7%8WBa1S_WFFt3T zBjRPSR3iD<;bm<~kYeplrZuptk|Hxpd>Z$%Te!nzR>|Jn9s}89`#QUa!zzUYuWJXm zP?v2T3gh`oru~8rMiPS;5rLqWsu5SkZRt(fJu&C3ZnZGfpuW8n_b#^ zpY?gBOPHt(Os;wSx!~D2BH-Pp5d;xkdkL~;6ArKhLW-k;{-FAJHZCj!84fNhbm!ceahmnj-Uaw*I=%Y0jQ;kcHfW>Hj@zC%n(+{!UlHCu( z8iPhI{^)JdWzO%OYOG!07^~$ehP4Q{sf#dEVx>4^4GbnpV+CRbB)55CkzTYLT0dUb z4q13m(kb(Df}ovEgdndrK5nfoA#!Op*QqEJypn_pOVBe#bXOs<8^-Yg9l891cWYO& zGCH@U7Q>UNg3S$Bw}ws+g-jpav!`!?E`^9B?l>{+D!*D|ScP}J;q{ge_mr^m)U6dO z`#8*@=;pI)-Wld-jC=weqNl6P)mu@z{uEpTL=)O{c0@Oy0oxiTve83^wQ=qRsjH% zJaXWr+rR`yn6ij-B!f`nn_Ge)20K7f_VwM?rqWiPz6~-k(^d|*p133UKygivv;sl8D0`?F{3+-Pcd7wo|l`vap*aQJ=tF_z^EHT=0V((`pJNAXrC zV;j+13bC9)n4(QXZ}|4L8JR8N+6-{&W}o#Yx#l3_TusrCMN*c_EmXL{+td0ev>T}iur6Y-5&Fec2|JJxP6f)VBqkWmJ)^xk2~TI$sa-uK_y9Du zC+f^}-r9U5D4J5Gp7O`f1yP?c35BSO-$0}`&nY7&#q~_o71|8d;RzcYzT5i=>e$l> z(mDwj$w4y@_7LJnOxknfpW!&@WO>P0p^VnrF*Kqmk(6^Uy7nz_ymn|Rd#??zT9bNp zLGU#4h0Zm8@Xg<csF)QO5v;ksXFS1roUiS))!U1G|LMs^8` z7LcZyk?n+0-_%X^0i9wrio%=I?-KXbOTq8JlNG})6#)m^2p%m)8!TbOIliquUOFbL2=3X@he692C1A;n2|#%4cR;n=DlYq|{M+Xmrimr-z1sW@1N$aBKFMHWChn-bI*RYx63gHKrhiHuu&c zmj${Hf+G9)rav-eltUvg#E}O8;#Zo#k(E%;%~zqjP(S%I3Sn1f0XqPj^`bBzJK%Hx zBQ0#3nYDJFRxY*;h5&4}(Sv2AY@S&5$CT^P%lp!a9<1DI`du{@;@Fs+*gj{-npg-p zLdjiQUEeMGZY{0)%imZmB+ESFRO{JVJ@2oaH}JxmFv_YDty|RgYQ~mplMlfZbsp$k z^(CUa?if#&z8rbpQy@zOJVR;vh9FR~-~0WLH!v7N57l6;m&!m4#1%=|^W%-j&p@Xv zsRP>;bkMz^lCfN0!U98@qr?M35*d`r5iG}`_z!N|*0Wo!XJ07$jd!%_eP(Rr#`tV+ z1Z@z|f}YX8l8SMinsJ(-iC zYSUS^j+w1z_d(K!RM08}F)WfX zi6ec_0}8vC9?3ZxBwLyChNd1Dhec<9Nm$ra3{9o-$>*Pc{z;ct8XW2eDx%psKzRtR zsk1no;3h|%U~oA1$H&0*jfPu|(&pr?jMj~-v>u&;Mrd5-e$VL#V?MCMsMQh!?`U!x zC!bpLR7QSwCMN5423i#6Okk3QXotP>r?nxf{oR?M6Sdo+*+Rbk&??FfV1Ri5Al5F3 zBy=Y`M>2`HhBT1Sji+;loetL5B%D}b4q72Yt12a1E0T{U&P~7E{$jdud#$dWC7fhv z!MmVDMps=R!#X{7UKCP{G-l2Lg+-@y?U-TaczX>93r)bej2)(rb9{mg@;AYBH_kyD zkm!!~0_{N8rrimyxXcm>EQW!n+34^wxd!m5B$F<&=-}XCG3Tv2LS7gN3V*IO#l(}a zqKpJ+ejPg68pj(~K(BNUcEqg?_~;5= zXCMpU1*CGR@!rh`+5!Vktc2X+&e~eJ&V>q0q=$}5yFg3loTEJ$4=fBoTbouzHr=V-Vd8SWN>Rk0r(s(PGWl(l^sat+8H z4-Hi-Ubony93tNd|TeRJG7yA#cw5d9Cz9RUv4y0cYuBylG_I#mvB!RMdz zZTyEs_+0}IX`6GX_t6}fE_1Y-Yu>rfweo5@v}5VB4DM%v8-BNN(&KVCC5SgR$_^V| zeM19>Ax%p4P_@@#aC$j|LsyC*`@lAF6_6n_B*iE?!zF*z-k6kWiC(#aNi^Z2<>tmSV48ifkRz3wee~0fA+P!UvUDd;2 zdNoJ^58#KrzPBE7v(75#s*!uIu2`_J@X=Cb!F zt@kO{n}n+bNQZ){qm}Rq+4p6q-yZAhuR1GgZ0>0qH%4ny$O52b@sv>d!>pG8e zlSwyUH_q4wsQP?`G;MdnXG~BLS)U#1NX40y+)&rW(;90tjb}GsrK9H8_Yv!Ft)RCl zrJ8tfsx`^-XOq3&plUQQVeuhou1E*^2kFLecMxSn(jPq1!jKQPc$|^lg6T)jS2Ujdzd}TQTY7Ir(88210aGE-kC8TZ(0*hpq0Q>df+R+8Ki*Z%Qv@MwtF$5U^igwrFfK&grmN4O5DuX%)ddK>nStHX=gzQkxIS&zK*t1dAumfg!xdAkr@L}ZRdfyWly%O z&9WF4C|!YLQz;~EslzsTb8Lbu4uK9j)Zw+}%p<}j2zMgkABjZ@lS8@$WD5N_!7o`zyg~OqqE3+LHz$_;@vp3ob%Lt!zWx&U z_V7;Q#%4X#P+d1cz+W**mhNfO38u`Mhah|ElLSP3&hx(gFDY&cg_y}9`v&OHKk_jB zBd56lLQ8T++L9#W3?RzUyPTdsn5 z1$7tt=v(Ew_}O9L*>>u@O2N|B{NDP!&npjlYN$@{aOr3jzWP>kIaV8jM5(6lLFMmw zxYXC@^@dJle|d1I)cu`Kqolfo9FQ=wO12&x`{1Alz5y`Hey^`zSU(`GZQMJAxp>mq z?NOTUv>xMEHS?WR*(TY4|DZ!KRm%h2RmkHC%Wxah2U(&UWoeGl9<9;C(3d&-5}duf z{(gY44Ty5TCxrRKwL!Jyce->Q&1xCbp#KuR0+fBqZjD>)b@$=scb7o>DN`lea0+h0 zd)3}*5A6RjPb~k|tK7 zJAag5NaH3UVNqfjQy7R-#iRYpsS`PUXG+yC(kX#XT}QvOxVg1jtMrtxjI(oVP4wn*vVB2C`QqmYv=vb$}&L7 zBNx8dTDxt4tGyV$(An5wOOf-_$>6jOM@J$nErO2X zI*WNkSHAQ&pF`R=|K25lg?2I3>mlEEt!;)Jjg;mRjK?~nZ|)&F3&O4|_fYW!0|PW$ z>SRHE&+`U{`i#;<=9# zuUR&A(737tN2d^0Ro5{@-`(OkBr<;Fi*1Vn;~}bi!KHn)s$3NHPfi z3i@U1_$0`4QK<-bN*7H%?INWzr6{EH?j%z-f=Xzvg}4+A2QY*}n;b|pz|#Uy!LzD`?V^|@j-JkZF3z}q`PzAt_qUr#+iGruR9C>i_5&tb!*!1KCnEy ztDvzQ1bg(dA&O26%Hk=1;SLG1_EZtjYbAdXx7h%X-jmV4Czr_0~75?U^T55 zPyKo2bv1vj9^u0Y4(Q-+7wLVjetBH5(p;Aw zu77N<&cs42sC%sMCX&O>i4P2F(yS)f0ZrfJI8n;`5Vfd{BCaAoPDKj4>^=={w7V=! z9hTryTh!L0J3$1h7_yDBN4UJ^B8)bUG4wP@1WE7Wk;uBglR-mu5C;C@==QmL^+4D@ z`3})@m;n4l?9rp+t$205@n~Ke(BOsuzE^YBR6he(>26Q_A#tl8usne9p_{_Ra2{#^ ze|8FT<0Wk+b}}4l$A(J1^1m`@i@J{Ui(xi%8scezTJIneCL9?(r4ElXGk?dhQ|ud14h%{ugE>6lXT4jh)*_)4{$g5;Z} z?rN#`Bf;g1rT?}HF$TT0&RXSI6?|94Osi!zhteMo5A=8gH6@pPHuQV(VX6Sx1AkkR zJ5YVI1|19x36!bjQfZY1=CB<8*idgL;LDLJ{FD=fWCdb6-N#CxuB{2>r0C^{8u8yh z>rSO%2M23imHyHpSYdZ{;BfVj>{iKRhlRfEGquueE}JjpJ33TKj6-!@6SLogwfy1J z>R`D9Ug{O0v#$K%hizX+ zp^zTcagw(p%~+q}rjUKOC;6Grw1-L+sM;)n9Vp z&#R@H*m9^LKvgN4i(~#Wrpe;OkTS~DStzv=?;WlNN*JCTqI4cn%<-^4etBzY!{rud zCm{m3aq3p<@m))WW%7MlnYE0{0=W8f4_k|O>n4c+%Np}N5yv2P{J7Rr>;KJrk;GBe z=eQk1+%$O&5)(uA>JiyfqE~yU)?LY_QeutYdWG%^_PW6C>QG&m9PDU^L{$e4m-@S_ zUb^GmYWPa+1QCC+-Tby|wHxG<`7onM(Cfubbc7C}TdRWEn?i7aB)W{L_;osUP+EsI zF;RA;#BiH5pD6#L)3+f|*feZRoX4E<)QJu}Zaw&-adS4o=RgMMutujJLLnk2AACqt zoXvv^jfpGPv_g8FSE|~7^T1hNXS5Hdgb(>P4{4d&49$qy6VaHZvSQ6|zG&P$)mqqS zDfSuJzUg~V$CaM5$SA2Cd~n?9g_TaSqH;cZYyX??y_M{#@OiMz9l}lyzLQiO#dJ#f zKv2zjUo^6VJ%oW6jb$dUFL!C!yyu>~;g8hy3S0$ZHeZ&xFXsib9zVK8OoU6yt)cG35`Pu*yScR91&29T|6tt#sky!K?5!6*fLNR zBp#~DYIK^NYd(WW`)lCk-CB4IA#hr?_NHmQeNMTRDSHw@-`R3Vhv7i+XB@~sx4m@V z><1A;wh^{4e~sFp8BM#B8r^1HMfA&P2Ml6^)DQEZ^A~1ii07m;fm+@J-8GmFwSZtB zj@S@wZ7kFD4NZg6O^A>E;j0|2b^Q`_A}1FmK7b&uGD{*vjwzryu(BZ&a(2S{Hh$%x zx6w*KiyBLMd9*rs%vkqOn{#CY+-G6_GXD{%{Zv9f3kul%^ezATZH=x%ao8MrP-kwq zloG`R>N(N=R%|~-2tulAiW7v(NMk4qKf{L3sbwdey5uxMdEz)#Z2*H|*#HaexNW5^ zk%!$m>t(@y^_RMn>e`Tpy{>b)iQgIBh_ZD+q2s!hn?r@OV#Pq6paSLsTMi`@9?xLH z{bh&L+cAj%C-v8@dmv6YJ;MFlVSGixQW!?D4;_qs2>sAzB$9^htM4*nqLT6<2U}zM zfz$wAHF?G{hMzqTByNKl82C^*X6>O&=Gur2S&4Z{DNk3qw981I1#}z za_3~E^aNV2&^I+IHRX4ZnGz`0gk#f1!UzgY!~#K@nipadH=>y(-(ygQ@Vn;UMfjwhnwpyz}+8b5?QmR6jx;i55< ztUnE*g)+G-88fI@79ku(!3rI6)XoE+rL$>Af{p?R#dcskBW@9v2RDdos?uq;@%mZF zaE+WYe!&zr?|cY+8#3ZB>Xb-H6_QxYQ^G(heUqB@?^sMCSeD0Bk8sKm>Aj9G zIuuo9h^UE0J~T~1owHa;Cb*L9u2#a!BvCaf9DDW@aBG6(H!iAu(eX?nWp}8Cr3&ek zw{3f>ZhQ0rY69j2OO(r`hf|l?s*1^MI&RccqOvWj4l=+DsK!A?p%M75~gC1hS{ z37N1Zq<6IhgIsXm&>_~e*j?bnwyfq%+6{Gzc`1DaAOyS+zi0WpZ8%%7h#9~I$!wV1JGp+7AURO8v%f@OOQO`b!n9>_&fO z6+~5`C;N|Ct*(G+nf(>Gew+8mFS9!P`JJk=xzJ{C9rAOkGEDPLm%rZ zsobFrcn{6S2%Z{bm*^&3!od|3xEV~R>yJ`<6>O_neDfdE|No`Z{}+u3F{1D@*5)2Q zv8{5hqxa_A@YecMknmeCpPOwivTg3J9m*ZaFl9Fz0c1w%70U&zY^@GMzBjTyK~6Zk zpLw8o;9_%SkpaO+JE!O~^nb&lKN-*usv%PwywJ`4wuQ`2F z&M$~U!l_7s#ru5F@;vSBsva5=nm!26#Qi(<0?ibWvvZCwey5aZ_7__#XLR;1w9wEz zAw2w05*)a?`b$0DXSM!MlbyBNV6qHzO^Rttdt&lk_}~2y2P~)iZgpVL>t!y_wDDYD zStn!7y9$Wd&%tBjnrAhLS&M~2tNdB_`lH!x8!h0{vp9+XtT6+v4e zoP_y%rg%_eVi@tE++7+Ncvt8Y@df0*RV5=3&}KlR;-zdYw`NhRuDn)Ew+F(yl}@K2 z-G);#B$f;b2w31?*L?UCyu6X*T)&NBrv}i&kXH#^YRJaCu+dxx?{y^QzqI z$n!?0VjV^&d*Vo7kM%Lq6a~$?WM@P2b7qtBPlQ4MouqBMi$hHTBOv?2{_cLSR5|tz zVLb>FI^|Vs^8)AuU4qQ`K%1%&EhJ*h%?`BX#3%?#EWk(^{orRkB*FGKcR~*gH*1rb z19Nc>3<;A3JJEGC7SoY%u1cZo1a$>uWE6y|adO1)wNwHHgWU8=oh6{FkaUrXX+>ue zOb9|U6f_$r7o18c3BWbzlBj?oL0X`*#i$C9b7zQlIF}PA;fO`#8Kq;kXl}5ZF#Dth zn^>rX)IxBw+J58>)Q0-Y-tRyP@&++;0`*S_2P8ki*@39B@tdLk?%>T}BDU?_CFW2F z0Qz1#EuAJVjYU!>L-53`>vmC)7$P3#t1)iTB1(Kox>RymR4it2j`d*4UBk&}%anFJ zw%1~`PfO5(kZlLch=dli*#R<8hdgO&Ay~xF% zwNHoolKVSl$V6FOfAS||!E|*%vbkAIsJ0Fv2IGYM(W}S>gs2Z5`!|$-*8d~^`(s@u zyXmcTd#{xYtMZzjW>>Y_OZJrpWsdPb?RqKtn7pAnh6NeLhQ7wbS|wsHSaG$YMD&>D zSdkM>_Vw4K9UVNkfQ&A#!igab1;W$dh6X@+@bhoEV!Y#o%`$4WfxFN;upb|m7ahD! zAf03h5n;~6U_g~YxRtgwr`O3{2rg;^uCyj>ESOJHr{4 zSj3CrLqh44u;Ka}kZRd>NV&cQ$K_ahpE~rGIPB1~= zI_P~c7RJW<-u`&G!LqJlJP0G%z*T1|5dMCGOQ`RD3C zGr0L!Q&W3M=6GB0jaL5FJz%o!I*C%fXROcO;j+$V5~bgZP!onK`XbsMkYg}$J$QVK z_b_DzA9;e%85k5hR89n$H<&9SkD)G_q#};9MPh*qhj=&F&Wmx?<#A4YT-TY-nf@%W zFa+1wZD5GS`gvUlO$JPo%U~-@t17GV;2j6J{`?c{3$@`N}ZmqjT^1!m&^{$-sgIh#_4-B!}n{0Z`X!;nWcIA!qwK| zB0H+iNqRiWhYXU0#`L*51uINXkkC6&78e31|LC_VE{dbo3Ol(vvxrI6O3t2F?!#rv zJfA0QyB`1M<&#A8RLv_#7!A;{Hb?yM+P=M!Owbg^*ojdzp@x3u$syyJ87wRxOl^u zqL_nCCvnGdla{iVAmvaM%vYk{x*Ug^!~2}qAP7JZkkts;DcTR>JYChZtaGL6ojTP& z9gsLf1oX?@IQv&+bWgUJ?v@mr$fp|Ev%FncYTzY0|#R z&(g_!GRwYUZ}kV>=U!PA%p2fO{|8RC&5EyvdzM5y1G6^#FW4)8*mt-z;Ni^r$c070 zq*VS?&;~YBYD@60mAVJq2R3v#Oz~yHc{boa44vq{NVt?8<83&=;G%3TVsb&uIH+Ag z221^ilz$j#bp83|P!M(s092~|xJ0AqtaewD;<;BIto6%Kvp=y(vv0z8QtcQ!2B=at zOp}&6TMteNx*IyNYHzuFsNxNTJkzH^5?xQ%dOVWR7rp%Tk9F{Ewv*SAXw*R)8X*{j21C5m5lIms?@&WL*cPbQ4? z4Rv+{M}~qFvINcmUersj2>RoW-E`SMUuRs?TRV*){`;o{0%u9+uR9rjMWj_gw5)VpXY z#10X}it+kT7$LNxn~$dt%I7`Zd<+_=-y0n2cQ;YL{8J*x7gB*Tumv_2o(FZ87Rw#r zNEJbJMFwTU?-fsb%U<2`Q88vJJ7Sw_bTJ^68ZcJNK0v@U1Ba<|qob~4l1Y!c7Gj-M zk^+TF#EP8hyV}es+kk@tzf4$PX=!OEMppR&3Ogk6vh0EX@*ZY*r)(p`CplxSzA%|j zgmxO!k-WYCf6;5^&KCqSY0-^t3REy}pn9osN9fNn{R+AxfK)y_t$i48Sy`y!oe&*$ zxue)IatBslwtQB|S_|e}M_q&pZR2|7rV0osgH7fsZBqa--1KHZQ4jO*u*59pn}bH; z^{;`8!2H+G&uDKrRL}VHPg>9J1(&DGW(qy?pa)X`+AFw#cMwmhT!}7&py+me_zH5_ z5MrN{!S&)2{WNKGhRJROQF4F(r_5>XpkTNL8Tjf-82=>^De#2os`YD`#Zl)YK3`cx zeUs5a*s>)$J3d)WfQ~^WT|3t2G5@b9wY^4%U>Q17aG5fclT7$V^$)Ii>q?or(k*w; zM8Bkgf$E`N=@6a>YStVrm380`lzMyN1OaIotYU0IRTvKX1-84cCqY%pSFnR{PmN;>R`9`lh=L%3Dm)VTzp>b5hPVSR#|0U+qbVOlS>EsePGLCcCe%_t3ufj`Y5CdohjJS1Ok+W(%3lr1Y>AfuczB{Kdp+ErOsSeC6mkl6e?Q(8s26z1$^*t)ym-E*E89C3LliZ zKNbl8dZ+lS|JY9zulaiBr~S1-wM(z3emZct_Swf0IIk;q<);JHKX|Wa^2Lr+_NVgl z9U6GOQtK7Z2akz1z4Fh&(!i&3+PP9GU+EAl?J5=uV(wmhTcN$H+*T;33YC1(%cnbj zs^I?mPd`)hN~b&W8t9*ty2Y0L(vJ`qM8X z2kEL5J37Q5UHNn+UCI@TrS^0_pY}TQUN+O7PO(9fa+v0VKxq}HIebET?5jEm&UpE# z@!(_ACH-b!=QB}XJ0oHrk@Lv6=bWB0G>^;Ig`u}C4aTj;%~Qh80#T-ok-MOAZX2k4 z+U8GaM`Aii$ZQBs2YhlJJ4`OKeR0!&ya6M$o}UtSM!~!{ z=Dyzk>PlcP88Mgm_GaipDz(GVS%{o&-o7B$q$+$F|m|14C!U(DCif(8XeY zhoN)Z*ER&nYUguQ%jCYH{mc0!iC zzp((JolsmadNcA8F`^r@QFHl1%+bu23dPQJrclfjvboNDAz$grwHH&JU8#JkQmo|p z(aeaWDfmFKo)8i@3!EcjhybbwJ_hjbx&54Twk|9AAG?(ET$qm+?;K$J`>+H&Ws3YhIJTt z!8$DDvrdP3TDU4m45$YO0&3N=yvpH5Q07+gvIWwQwMY!s6aM^+G7vSf7Cxyu(aKBxWlYe5eE7I5R2>0%_XIpc>nV z4U~_;Z${Fg4{=z4773jI6j+kS^U1}=#hJ#`OMm6QFpCNKTbL=sFNRm>g-x|R`U(MS zoj7Ph!vze6MAgORkc(*J%9IxcH7duLYOh-8{ zgsf~iTktAH&r267*{-gRv^9=m>|rDsH#$4&j#P-^9>|kTGs3bD(|6Qs-gzb)wHNTU z05kYbx!v*?ro(JHli#sJh#M6+>yHSn+E*bx2V1ZqGO27NWl-Ni0SrbK?5C_NAs|N{ zRW-8PmGhK!l`9nD7ruS@nJ`IjZQgO$%U`e#+dEP_Z_UgiaJXT+!`|_3i#v2H@C7hM z^dZZ9p56?>s9*~=M6o>+$?q;dl_G$HsLoR+j;7N2IO#cC$ai>!PA}adlwZYCHeYGa zrOV}9AzNur7d!1^CYf~5=?KIk&v{?OeEP7he$}StP}m4f1b@n zecoJKu|B8Y{PTP+^3oM%0Y%BfK@|0xcV5WHFRd*dySRMt)^HZbI|x}`UdY&y+pn^U z$s<3!+FU(viD}LY-eE^Eaz@G6#?AtiPz$3tg20ec zs~g&kjnuItJadhu5h<)Ha5bkF{ktavBT21oXfr-iF&%|Ljj!*vHm9KqlK<%b*79u! znr%=RxpDETu=TUi1K&ANO*r4N=@eo`ymBSeQ4u0$rQj7h%0dv!W{O>zjxJ$Z$>z#l zmzjeU@M1>EMsB3DLf9MtA7n97;<(Jy8NPUDrMv&$nqF>~(*x#ODh8Cc6HK5o*TBVR zyPDqazvt5n+{YG)EqG<2H%e7f!njs0XF9T(e5aRfZ!c!Mih|~vx#XnHOC_<8^-@MB z?NNeST0V-<*th4?UFCefv(!;33&TsLy`v+QDwHzij;<0@;OCSG4zhu)h%<7}87C7b z>h0-V z9C7u!N}YlSDz;b39qG=JSM~~-Ofj1-3TCIhRLNT7#G*Q0s2brzE;uojj_(NJ+cPm7 z!QK$QJ(G)8a3LGBJKSUp&F;}u3}4t`_)O#Fs*ovA*PtYGsi#pvXfpS$^5ch4h)`B% zPoMLd#ptV$^Pz0)5U~=&{6w{6tQccBx_G=U-))^-2Ny&**jvl8PC)DKS8+VPG^5A%B8){*9kHpl=OX1g=w62H zv9JB{$67gdnyF5A(Uc9^(O%ebP`;Fqft=T-^Nx63V5%$M+0iK&yi8VDdMcSxdnr{G zw|}WzDwZm_l1-W<5yTL6C9+9hGfBL)66r+r2)SJ3jhi(i5SQ)V zzgad&un`-ln2TIVPLGqZRuVa8j>gPb3;U1Z<8f0cq>9-c5MgB!JZ?ZZ4D&4`dA_(|g6hwmi9tx1JUv{pC6w^RC6n?7Nn z zVi#g`3pPYY;BK*)QDIiARN5=q_H0*K*k*)kzFZKrFq7&KjAFWjqkfKogLYn*OBka<=cgpE|bf3l)PeBs@;%pnkI-6)!N4NIl&scdl&kSd!M$)Tv!jKY~3aak4lUJy(llKRe$9ozq=kvSv%ph z{Li&PA|V5of+tFA!NSf|I-Bl@Q^6H7MWHt@6)Rq+ zP@<kZ&PHPf z%tx6|T}d_coz-t4(w72+8(~YdR6d~HX=@_|2HFr+_@)#-PzZnIN=aaFv{XXSSK=3m z9Xo?~F+TBJfcm-<&p@w`+6Fqt0GE5~qHn#Ry(~e6r^NQEm$tXHMEl0zT1XO#+J)BU zoH%GSSlg$Cfwt&%WM50{v=dH;3DIF9W(oQHPD{wOiR+$Rx8xk*#)m{6QhF8)M87=b z2v1UyJ(@M&1Qd_BqTFiZM1o6fZ_n+p#C#i+kp{mp zf)*sUhM}bEm3xK53i4u4cKvm5b{#v;t{rCwJF74RsOBCg+O8)G9VyJ%O-CVfF2scm zWeO!}Y0RZkc_EOND#cu}QW8dz4lkAKDyC94ea;*DtP+YjLlvT>C_JZ+Pr_Fg<)o)p zU%ANJ5|-=&&UwoFDyE~o?Ns&Iz$VyFdFWRr6)i%k4GZui_EX+hHrj0n;<^2$1^@J) znRA={l=qd34q&ths0s^^$*V3b%fJpek*dOxKl9SvnO%Ov=h2>v_NG;(9SL(xd<=ie z`|5~|cuQqFa)n&EU8oa;kvu09fL^&$Nf#@Hd`F>Fa?}d#Qg^KqPoUcO%>Bmrm;Tj< z?wB2s#`D(VMB@nyO{3rT_mQfQKzu+jAV4f48Yi^DF4QRJcGJF-c`FlYPII~m^56k0 zS*StG?WVZXqH>{DF1wp-dPmsg1Ks3<-DG0@FjDQMvL~z}S@CbD6c#!vUOrRG~MMCEC7xqO$2q1}oSeT6o zbJi!-e>@5))HP?;e6T9*&4T?^`WwiA7J8oPQ_XUPej4B!gjQ!KcfBkaJxR?E2@KoX zDs8eFy6RlQr!8V~S!LEUJxI<`cams(GeMmsL?;RAgwHXP%IxG)2$m?lSGX;XEtk<7 zXs$O(YU$J$LPZy1;T03e#)-a_PA`|vOH$&cJIj@>bXV5vD79xw#k41M|Lr!i7nzFe zk-u<7`sAx>+0vb1>CyJ(XUcu_Ze#pq^D8G}JC{wxQM6n+oy`>sDWN;Bq$}lYxgg|l znT?~=ksTIA(ho))atQ^0Ga9PsK*Cb!9qo`E zixFy;k(g)z5)@2)dcE;|2%^OYTQ72a2VhrX`gY^+`j^rfHj zzS@hqSk|zsLmKlsyv}Ss-&x3%iv5mIn`8NJl2O z1K|l{(%eq6*_}EGVbR=9+IJ$;VGNqvNg)oFrE*%x7E*iXrBa1#rBY0l%blrmO0Y(S zuCjBqjD@r@@#p56EQ2dm{wjy3Gi)Kd!-=3CT#Vo^tR8baAQ;WV?k;3@?de#z?GCYc zFWZr?bmZH`X_KDY9bLt2uA{5q70N}=21?#JV?f}Vk1jPY-gAeIW@ntQtke?`eMOQl zs52kv%YMrH3L5@b(o=AFjzMW`9#3|PoVy8S|XsG3RsZjFiI z#@qxbyGcjn&rUTTC>MEEMcYzUymA-zEDPU$mQXweVDTzbbiTI|+~nf)3gynuqPRIb z(*j?FFeo@*RQ{7Uql zi@b;YN0%E5&xMG#J;wYaou|C7OfPH}OkyV8Yr z!K-=gasgeXaz39<+b~tUu-enaZwm(#kk12y5NA%KqAwVRm+r_#D-AIzh{_(lwJu%w z0|`UycUHgoos^MIs9@Y23vo0-6eR`lHHv>_sG!RPy6NMFFp3}-fT;!+5|~#lldz9tDV9U z-9I2;;jdh5tMz%k{iWVd+k`wV)Kh{t&4cx$E#G*y+IlJP{I<4y3Loa!z4Y6U#DY}+ z2_Y7W{u2jS|4L++LnJ;BLwqELNWc(@e@Uc;-fbLulq$Fh2?RZuc%4s~*STE41*W5; zJrlj?3@tj}<}bW}94^A_)Vy#@KoG1X7}!332|PGu29^`c;I9h9YOrNl7xrl)7-TdI z@`+a%juxr`DOM{WmrX>CYb7^}cG#+AG~zZ+3sSl0kBrOQaTmuMH|H8xHd<#@W|FH@ zQcHjLrg$#MzCZd@>J{#cGnCCFg1yMy+I2@(?zBuH7Fk=f>(Z=1c{yNA(i~Vr{Zl}^TE08xnZGx-&&d9TD=14!DI=}sWE}d zHU{r<&_p)%_hR8O1B(_3F|ceDF}6VLzjr~BEySWS=8jXWw>-sQSOFb^EYWg)qxEue z`>QM4ch3sNk5Jr+f5BQJFizaV0^R)6x5Ny@-BE;+g47P?-_}=5$pfD18L-is51mLS z#p2l5LMOPV;S+C5)eCYt-} zuazo2r9LyCkHmxb)B~%Be4LK4J(ck~+68YV6g@(b+@2K!3ImvsUdx$GS1HTyaTP?e zGT(T9ys>n9`}lchplnCvHaH==K&hZB>AeS;=gv)t|8H*0XL7|3p&KYzuG3DCZhQ|y z5o>p1O|w|s?KG9v(%)E+{^%R?(US?ZC-Bqek;XA5A!k^t~PA-I99)in`Qxj&=>GrN{PITE- zZZ8O(jJ zE8;8{(`9ka%4Hk4Dl>;-9GYK$ExixFzLWUyqlCCp#1V1FEkw$Et%dpRyQ6{$fe+iN z0|P^zmGzXrPP~J!t=@~#>L=xU00m54j|6(k`zm%siv(vTBxrS6fS~{<8E>YAwFByeD?qnvb1NMTW8U^|#53 z?eR0o58S>Z*f%qhJW7Fgyx;cdAVOIgt*zfXTJ=6N9UhQ5iayd0c~2ebm=OAqmGW5n z@YlTeU~WZ=7bwdlKBaVfw$h$S3teYNx>(9o@}1dYM^@O3Y?`B7@|8tTu(OI3NT&b! zap{k!t`v9dXjcK~hQ6|5v%>pokGv-o&1yq*6r92JoBsTi+qIf*BB~J|sTdt~jUzq0 z)3~u2=vqIu`YJ|Z$?u1}0okmr*5B4SbVwjfU#*|raQ_S6Cu;qPH--)g+`;!&g5^vk z{ps>bvc`_+SB+#kej8qh4)qJ(VKP%nIU{DHT{7Uq*73>KvoGj# z*Y*+Lh>w(uc46QXl|t6pD?;h+PhE7`%+MBA&h1QOXgzXe0ancIAsd-@w5L?<9Tfjr zoELv)_YFX)C_4U_|c;&&i9#zIkHWBKqp6cMB$GDge-#d`_*@w_F@gB7liYaLM z^T3#q{JZE}ebdh=yLn{wl53Cdg*0NkAO6tJfF60uUA{Pk7ZztKpWk^excND9Hww{` zP4sT;VLQV2qY$mxqy6C1kJ=Hz!0%#5?4F|bq|gyb*+LrP8D`OIeY<;J5}7x*|B?y+Nol6zW-fFZSwoVE_Nlmi(M(EQ?I-$nQyTx#dLn> zUCHjeE5%H1H@lME)vjb>{j^hFF`xH3I%V3lY)7Y8>g+03%F@=OKSu6=%m|%jtN`+E6lWK3zl)JL2Qp(E;J()19@d2fsALwlCcpb6+b-AuW zMc9b*rH*Vim&;Z>!Fi`;Qh`)QXQAX-<0(yw9#N%`ZY^xI7VbzsLD&mszJQT5)YC_K0? zI%qpo9XveL*;Xnm1{k^&0!-ULZ>ewKaBZ+{V6b$k+IxtzNAkl2e3;m;K1|Su3H@Qh zd}z-o(j;OA?-#FyE-*N@8RjEs5;aO8y3~LYi={1ec2enyryH|#&1ZMHqj6*lK1Mp$ zd!*PUZ1O^**3p$M77AWo=#Fw}sYfbiy0RVirb^!t_dbfj=X53)XB_Ux2y?I)xR|Mk z>$)RX5tip-S4qf}1+Tr`#^(-&&%v=7X1inOQ{Td2TYjI9&m0g)*KR-MeZ_g@pO!hR z8cQQGK1C=k1L}IGK_X0jFGeWFk;YD8+0U1AxnjB^?vt{(Dod$sm!MMO2zzB79V;O% z5AK1F-c;@;H_wCuHy`ajR_?E~!IzLpnh|#Dlit{UcEoF6D_&oB?HJr~G>FS6y-AJO z8k~_jqP18sQcVAGy|%~qI_UNEb(aR?djA-k#EcS+)-Z}JbX_eAiW3K=kD$>(FAhri zxWk*x7E6_^z^05>Zm$SE^#8xLtJ!TL2Ewnx3m|#@zej{ri6aldVZH06Af!rgMoKFX zLMnd(wc#KY`KE@Gpvt7Sh7_`- z_%G$7KuZ{gi#+}L@w&T8(q&k1+F1@aaHc9@A@7tk*eA-|aySD?%`0p$V$gXrZC?F^ z49Wb%v~6&B^X>gE?j@1ZD@XKtG#lL1J4&O0SfeH)3A~r!5^$=^io#}U;8YeyS*9RL zVycScMYy_MY;GWmTJ;Sh z3NJe6_?L|M(|X5{-h8Zt!Lu#4@ut0Fs0W}(ZBQ;xo##?$O9`Cdg^-yo3L|pGO9Gh> zymXu&YPZXx+_vqtGrV}=E~7SFLH&(&(n83R8ctP3-*Quba&$iX>a?NEWKt-$BoY)w zSV`xF4Gg1Rid1Z>{pdSFXb%m?eGrLG!B8NRs57{Wn7oiV6^s$ityMxoGDk}mC3JjY z{cQI4WcSL}wVN#?S+Y{Cf)j)D(oqA!{YMfA1{uzT(!!b=)^%hgppcLE940Y%WPy0s zb50`KZcq3hx}`&QX+C)lzy9CZv@IA|oWDHGMuLsCFe44q(|a5meox~2Tj zbiP|Y?e0}LV0OVhn7_r5ds+0ZH^5at^cmn~XBRYgzyYln`Z^I9;{sOV> zMz^vfVp}aQmjRIdL|4Caa8I~vWxBV`f#<+p?nP@P(mBt$wj;orSo8kpFpI8l(DQ|! zo9^4x{tDpZ%J5#R;9R<-jrNGB&tlnc;_$HgL_am#b=Yd<=G22{aO*{PtJ>qb`D6D8 zn%uc^+|3%eo|rQlCZnfy%51&~p59!Bk@?OAF-zm5A>&BH&)wUZcUm zuXN^b7MAe(B!oRkN%a3vI$Nqfz+KF3eim;aNDEf>A?5>&I>Vrx~=#uZvzgvK_j zgTg;$u~M2A#37sp+id$?nZ@xaHUW=6;7b5dcr1LMn+RYTAuv%()dY}rVGvGe#(O|_ zK>BE644knTn7c3_{@=beOf;n!7W}&7;fa70Q#FFkq!gJ;1*AA8GDi|oiH70d7Pp39 zOMWLM9+!1x+5qyaISFex?!N#?o#i{Lh(m&cNfqI!YzBGI;%y>9GQ=pzr+;x!e;cOqDzO8vn)cpNk>=csTVs^&5v3-sAZM000TgYaIXp diff --git a/docs/public/search/fragment/zh-cn_2b50ca7.pf_fragment b/docs/public/search/fragment/zh-cn_2b50ca7.pf_fragment deleted file mode 100644 index c079a2917c07c6e86d2ba1260af339ad5d777563..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4703 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 diff --git a/docs/public/search/fragment/zh-cn_3375c59.pf_fragment b/docs/public/search/fragment/zh-cn_3375c59.pf_fragment deleted file mode 100644 index 0cd8c7dbc7df184da2bf0c59a9ccfb9a3330df2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3219 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 diff --git a/docs/public/search/fragment/zh-cn_3e505e2.pf_fragment b/docs/public/search/fragment/zh-cn_3e505e2.pf_fragment deleted file mode 100644 index 6e98226a4402c82ac7ad8d56de3b4096e409fea8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 841 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 diff --git a/docs/public/search/fragment/zh-cn_452ce63.pf_fragment b/docs/public/search/fragment/zh-cn_452ce63.pf_fragment deleted file mode 100644 index 6abd1330509bc39fc4b47ba74700736fe09579c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1960 zcmV;Z2UqwXiwFP!00002|Ls}-QxiuN|5s>!(iswxAP!<}MW?jRRCF4role_vF1Zcd zc*&)A7br5!L_jMLfhwX!90k8XRftur5E1?tn@hq^{TKRn_ipcd2#BN8`U7Ea_wDDs z{p`MXZ|`NeE|e6~{8>K1_Xo1F7-$PL@lrx*QnRWg3!JDlr6fKpq9BiYkSJwRD6JY@ zT$&HYIRyn1QYw{A3kgmYq;ylDF_4hbDhL8v`ObsN?AV*!*w);Ot=sd^C>JLycP}(R ztFk^_-S`c6E#0RQxVQZBl{R`88rsq%n@0vFCQC&uSf2j1Ja-R|-B{k5ze&cfuT)0| zabx6~cI7EFLZo|fPP=^r8l}~7>*L?K3+_?vPEji^VA$MDX>-oNVBHRk(i-Sr@dI;< zX-hY?*+q<~WMm=T)j(uNORIQ2SnJGG<>4!6{Ij0NL&d9=TdSC*nPDs%M_igtxl&r6 z!FZRRRUTbn`k^0pP86yOv=g&Y8UCx9!)V&ZW$iwhRh+6!E#k&(vHW`hTUwaX9u5+O zC4%r&H-CMCfmgG;H>>d9R7}xF{rWDT9$eQeQOjo%R^kUb{+8{Bh-b|lO)dR-8wKaHBL^5X*Fqu zL*Y;a{s%E&XZz{cSEtUnr5q;M1dPrQVHmFh{tiA=-%r^Qb6Nt;&&W~KpFzGRX`-Om zSySGgHh)%<64}CiHV?rskxni}Jh07Sd*~eMiy_s@1oI&$#Zk6UD?k^UZRpW~LZ)Dy z+z(J)YF17&GKhPVZ6&{)e2xSRPEqM;wJ>O63^de!=Y$$;adF^uOkyuY+;{ytxMKV&>Ea-o(~qN zyf)UyNI#_m^@m00=GKahF4>x4u$FD+LBJSrNc6cj$!&uoiOqZ;xGddZ9%V$S&o45v z$XCp>$Oreb$|BD8+fSePU!{uM$13d5W~N^&Kf-SBksbNQO?RYmztc_oomrs2oqpaB z>x1`ZDkNpq5MF>PrgaGlT~8uZS6r-O z?a9|U$I*%W2R}hYcJ(#nhk6wb1$STKIIuBh_;&16NBc2JQr%zp ztA&@=T&!An@WxUIBO)CL@o_G3&UpTgBVj=87Rdh$)cSNk%p|k;-tjqp{2r0fPO=gx z6T$D+O?N0OU!Jb!E@EqLt(3Zj$v(<&)p*H$1k zoS_9D&jTFSijv0ZgwkoRB=ctzQWjTFTMvaB14%)I51dM%t$&~~00EeTRzMY05#jPG z$;qKoMjKE;V?8uEr?t!gOy$yvZVBW3&>uh|O40f(FZ6&?frqBb2Nm#(I41{TFbmx( z`UT5COO&YXfwu5Kqji3ov&}~dEES}^bxh}s$>io^&Y|^OTFkZ0k3@a-pW)&nI+Z+5 zYtM?00g)CvKzGELPs_4F>UZXBz2?TcEl}Zz11oA^QS#|UiSCFA=s9LZJ>Z8*rRhwH zVqwf{%tVbIbfPvhRFaUuU5Z|zf}D|TCn4F!^?i#HLlm+W{lazB^|8~Tw{;u6#XL=Z zxQq~_TcF>QljLRPQeMo1aykVkOCIkxn*j&Ht@Y69+$7I26SXCBaCdYH=Xb&;k+x`U uZh1R0gORtT^WdSdum4C%E)Fj*w+9(-H4h(ZjYbE48u%B(j@?LzA^-sDOv1zf diff --git a/docs/public/search/fragment/zh-cn_48eabf5.pf_fragment b/docs/public/search/fragment/zh-cn_48eabf5.pf_fragment deleted file mode 100644 index ffa4a1b0200caf1d62208ecbe7ef52d888563af2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 621 zcmV-z0+Rh7iwFP!00002|BX{$ZxTTe|0=pqnn0V?ciDhaeMk3TuGZEF$g~{WV;D#Pvwm4-P*I6c_m@66+Ak-umOcj>NA|ymGoaqAS z-uw2jvGF^3x8y=wvRoZ4__^#Y*bue#rVj;79D5P??jpy!r%0JxpngeM)<+Q7j; zum03)?xOFtJ!)-9->a@)E+8nqaG!kzFoWg=S)L5o{^@NOeY@}5O;87i6*N}bZWS+@ z*d@!~Kfv_Wg8yOl%q>oB(A$ObAzXNH>a}z6ALTkgGL20L9z+` zOfql1?&sIAnzvo(I=bvlw_cRpZch_W%JLd{w z_5@=9X@;ucVv*QvE;P;8l~a7!FI>OCdJeiWCZ|mfh6yKFmS9s(@vlU44|9J3{TKBX H1q1*9o|-0o diff --git a/docs/public/search/fragment/zh-cn_49920b0.pf_fragment b/docs/public/search/fragment/zh-cn_49920b0.pf_fragment deleted file mode 100644 index 590267e0d840be3d210597f309fe87d0636239b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 644 zcmV-~0(<=*iwFP!00002|BX{$ZxTTe|0;S91Nw56c%U9`I#-@=(YIl-n$AMWP2^PAt!%%UU#FdKkzH z?|JP2xuP|$?>gMaj-5>-$ZWF@Ujf`e_d}<+1+O+IG@Ih3_VhLvhnA0d`JHVIY^yej zTX=lmp_(;T#=d4*UhT5sTL5SM1LzW)>~F&!9uKnpBD!}fEdP_`o4iyxC+0^hqb6?7 zi^r_Dg=euhm_$9Gl!Qg;XJ&MCT{(3nA^VfWI5LHPL`G#9)DCWKfd1PzV?+H3I9h!mL*-0eTk^BSPxPnet1ONaN9W{Xf diff --git a/docs/public/search/fragment/zh-cn_527e5a5.pf_fragment b/docs/public/search/fragment/zh-cn_527e5a5.pf_fragment deleted file mode 100644 index b5c3cd93020e5ad4140c204b28a587476b514fe4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4232 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 V4V!;m?#Uk~{{x3*!eP-30030XxSaq1 diff --git a/docs/public/search/fragment/zh-cn_5aa965f.pf_fragment b/docs/public/search/fragment/zh-cn_5aa965f.pf_fragment deleted file mode 100644 index 4fdbc5e6264f61bb8f6d5d4cce82dc75f38b2125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1801 zcmV+k2ln_MiwFP!00002|IHX}PuoWLuXNEq>;+5`LV4MnI#t!Ss_mqH*rsW!9Qy(W z$9C-t10ke9p+KO8tu)XURG`qZQ37jef$;WU+So}x?Jw+}yPUBhgs4(k6*)fN^Lo$Q z-93ibieic;-;gD_Cy+4JKue%b)+MVBC$MfRqH5K}bvdD;S{ZdBRX5^D!*ymt!%7_0 zN_yN7u@Y5O1$Wn~iiPU}H33Q2Fu@|MoX1P~nb9}N(c=88;_Lz$IN6E(T;Dk|%I~HN z`+vaH>LSB{S!Z*{PR)^ly*lC2QNZ{_E(?fGddQi708@Zj=6lcL1-psG8aUS-}GQ6%| zyL90?GCPr}BFien{NLR%uZaAkj6HJ4E6d)d{0DdK3?SHBTZLz*>+m+&Pldf)b_T{> zF@=K%#i20(yh+Xg#pa9r&bU|Fo8&Bji!;f>(>}Hg_kax6giu}HEi!>3z-Z+_YU%t%tVH%XszGsI;LM(G%+%%^671R zcf70|5ATtZ1t-Rwsn@Q?%*w|!&TT)g%#T^SklD}gWq|B6_%lqsQ<0I(zwPzgKALBf z&d4;d+j-y`3kIpV!rtDYGfwhVeq`90d2Wwy!u;Y8@fl$MaPcyar6w4Vrwcwpjr_|E zHNO}AHswfPljkcKgoSz`NX~P(Gj(OwU&Ww>fb6pOg zxlL+6NN{_6jsEwus3tjWFAi{3AbZL#qF!kBEh19QBCh<*p;yUMN1h86(A^X`?o|NS zlINmDEg`hWy961<#Dt1V5Lu6liU!(}=VS8pkf($#8iuM!r26TapowurAiY$Fi8VX5 z%#7jOUxz7GZ;iBvxsth^9hS`HRAymzBxB1sgNDA3cPW2p;1?HW?Zvb=MLe1I5XioS z)gc3EGSZ}O@Cl!7HT8t?yMPWdzhE ztNM7rgeZ#!DHf$KcSW4NpBI-vSXjjNCHCAo&P>utuai_PU$t3|f~!3IJ)AgmOVSNg zV(lp#oWccj{EukOz31B)Smb#N53AVsyo7aS{qU|iST_4CH5O^-mNpbV9r7l9Zfnw) zm?5FDLQY{lP6D4JcnjIvWS_Utxiiha3NxOfX|R;rf`L6dILo#HXGmQ%l82sM5H>WhZ1s(O_MKQ9D70F^t$SO+h~lB$rC zdYQvTlW$-4SUvJy)=)uRQer5B#X5^hP(g>TE|uqx9_5qa zN`?vxp^An&a&tJ`)Q0FOboMuyQAd1WR(dw z@+%*14MHe_%^dRLjuWE~`z6B2yDF;u8&(7S#X>Hk`7k_OK*MZK$fF|9eY2 za#q!_DN5wl!bJxwtt=?ihfgdHd>W~ug%!y{qA9f|ni0=4;Y~bh}z^ODjOA zMNMkcfz_`)0i>chYwOBNCy}ejWQgTji(GA^qFGCT2{MgQ7vPYQslOe2A*zP8 diff --git a/docs/public/search/fragment/zh-cn_5b363a9.pf_fragment b/docs/public/search/fragment/zh-cn_5b363a9.pf_fragment deleted file mode 100644 index 2412dbb76ba7f5003d96e84dec0322111894826a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 911 zcmV;A191EwiwFP!00002|E*P9PZL2D{wumqniNrtJn&)+iEmyK9*i-DY@Zdvt&YW|;`OanLG%EI? zkP?x5Wl0_s;<_qy3GK2b#o8q;V(3aRZYWx$tuL;~NL3=}vc5fr^e|n7fB)nPn>Aove>zhMBBmgCohO zNof^qpRLBqJhOc|@G?md#^>r}Vl;MyF{?1Fu516xNRSgTGAqB=7ZJ`ONFFP=W>LHYpFRTPU+#qSz0G|Ylm zPq$K$v8+?E|Aw$Q3nQ>}H>;`XK2x_A4l9O%bmo^Ucc=Lp;Y)UC;AcoSDf>`Q%S<4T$k%icGMh zevYy@rXc?X+N^0tSAZaJanrTy%$#BLc}7!rDsVYS#w5jZYoB=X&k!==(OV3*!vpk6 zoBymKi=po3HaUZ5rdrR&)>n5Lu7l!Y_jsBoJ-#qAZZKFxY!Lr!a_c+%!W0DP-<^O__t zSV(4%$Zat?^n;?_MvZZ*HqZy~p^~aeBFulG>;6!n6(12V;gQ7t lJ-GG;aIp!F>Izl(7(VOypudEF&@uFO=nv&L^+Uu4005E+zp?-T diff --git a/docs/public/search/fragment/zh-cn_621912d.pf_fragment b/docs/public/search/fragment/zh-cn_621912d.pf_fragment deleted file mode 100644 index 4dd02514088c26642122fd9bcfac179ccc7304ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1846 zcmV-62g&#!iwFP!00002|CLx(!n0 zXJ~eMNlcp?7g1)=(z-Zd7ERs0h~Mw7H8+;M`i??2fUU87 z#hYD$>HF$rEUcZk_i%b+wK0D_jEe_!XWg6nOPJd~0`794$y>PQtzSUO&A&0S%g?-7 zWbmf%cpLQ~IFxroRgJrk@msnVXp1JvtIf0?P8~W)B0ZsBhslRG^=9YYb#(i;^W6Gv z_&zP`bBMt&9jzq&9LTxcZDQ-+P{##MnDomCsbf&rO%g-5NhPUTC7}1hXFN>|yGYW= zHwGs~%81l|L2bjTl%V#JR-%k(gb~SOkUTY9IU}~694qIkV^T)lq;B~ZOj4qfM;t9J zrnQrtZn}(^YDZZh^-~A|}9z=F8+OQq@@5 zWmZW)d!Q4lLt&Tp3LoIdL5Yh6?@w&U8rL-;>)KS6vU%c0Hu6+Nt5yLUx+;}1OC9sa z@hxH$%z*nrK}EC&0ra=~9Q9{BUXdTeC!I_s6&xc0pjoaJ8MAThC$2YEz;YjLYPwe0 zQEAc&h^i?ADH3zENOY|BO4(H1NWI7MZ4-42w$zno7|HXs-aWaIvG`NzJC2DXla|-d zCVwV8UI+50t0Xua{CrvMM>xnDrP_dH7Idd1Nd;(4yr4!MQW@4u^f+Z_C^e50rWS`N zREicVS5iEOE;5ll8uBPIV3m2*gx-{GP_5BH3mgqR;fKyJMp%)WBW2T{Mqsktb!uYH zVhu8tn^ioI$3} za)OdkGyqOH&S*Oazx-P@YS_cY$f&8EHI@*V=h z2nTR3OSWA2F#%snN;x9oDeVtZNEuG``FNUD{DGo;&&Apy`#{i(T$F-j1cJ0c$_5kA zCy==ov}YeK`U~)w#29u+p`fdA;297q1a2OZ&prVO zHgp=ddU9|)6Irgr<%a*5H)6o2_pa%~R z41NHmsR3u(a^kmuCCDl^4QV3QF44qU zIY^IT5$!lJ@E#eMItQN#9Grl*n%N)$kqgXv>?gi$@{y zZq!?~TI4~%tu$u;XuO>1Xywn9s1=Yz$0n<-)i(i;Fcyfn8G{#0t2)IWTWgK0kNI0? z-djRkYpp(x3d(oeGPE%s#CEN5EzfyzI@6P)Yo%N$D~h5i_`D-haU0gL#QPF??kW~QJm|f817nd(Gs_; kP+=Q2RHP_MCr#pN66y%%5$R*8iJvC^19Rj&=Q9!j01Ir4od5s; diff --git a/docs/public/search/fragment/zh-cn_643d5a6.pf_fragment b/docs/public/search/fragment/zh-cn_643d5a6.pf_fragment deleted file mode 100644 index 6b456a2449e7d5afb47c950f2cfffeeeeb8c87ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 930 zcmV;T16}+diwFP!00002|IJoePt;Hp{wvxiO=f{{i^L_y=$rZ=#u#JBo%RkiblRr9 z1q~q?2q+AT;|4?^FzhZ2YCvGv{)^I1dGarK&b>3WAQ&Qv_|V?(p1tSZb52cCPgAO{ z)G4ykA~}{O)kty0knK2gm|>|zv*T$)aWon=OllFP8hVAG>M6rYb5gWPrOm2dAD1GM zZ0L;Y3=DZ=)1{G~Z<(H*{L;?o1OTs?D~-3E090BZ@Hbx|X?{vbAk|x5bF<>oTpNH@|8-Tq`J2K}}pY}n29m1fbO`?43g*{@!vtzy2> zNm7|<2)F(5qLxN&J4Q6sBu`?pkp_Ako+Pnp zSyt6;MszvYn((?@Hf|b9m>k$Q5Xg?rxIv`IhUTPoyR3Wr+N~R~)Zo`(=#&wKFcQOH zVIP6#Ztb<&dv`-Z5CwDWm{@p*y9SOu8|8q1&qtZ@k27-QJLfOVl-8aGS9))96>7_t znxy~JecS&U97Cjsx(nX$me)6Nz~}IDDq{B;$@nvE;S&|-6dWg)|FPb?zoWz333^P~ z)~=lV!HmDu0nIkB7gpmZag$OdN#w@L{yyOslr^Y)HDNB`+CVikSV*gd73zPAtI$zjZw>%YFb(&5;h?~5>uH*ap3YUA)14@2@*&_ z1vD6_@~}ms8cmDgtf)<3UsV7yC2Fe*O%f{#Ob5t}K1LZpnj!NEFV$4FM#B0!*`*I< zjsF3HdI)t=Q8q0V2!W0m=3w!; zY}w9Wsw#!9Wlfr-8FXn`({1fqR$xssC|m24_P;=`?L6@pcFw(xV>hd|JtY2o?)lC+ z_uS9tn&bwZ(emnbRZ-`pf~8B-Qd%_>JM9!4!_tUur*no{&}qsrsYM*k$fvcuV&qJ~ zSukgbO;aSVrq108<_(LeG%Y11#mGA}??6cJ;hx`I`M0>z-~PM5wFAI&>;9u35`g@Z zMsRu;O$WQe1FhbRE-OC*zz*smk1wp&doDV9jXU1;S-+P%Y|ynvweGplw0 zjG>u5bea1E!A})j+}~zf>i~L3RWvT0u+}kx)w}HI_JHIa?Dc=&MU)wsdS@+f?Km2+ z*L@7Tctzxa{r?lR`-{(6y@qa`AER{d!6NIryb_o12ut=$%U<&-g8PfOyL(M`do>It zB7}+FB*L)B$`X&W4F4aKn}qkK^UT|5C%DB*M4U?h6&K=tkVpCumciP`%v}>1(c@FD zg%7Ez8@F;Gfic^p%9ulQrcN9x-h^JpKy!(U_4Bevn<+|T%?{ft0vIW9A}sKqGkyBFy^cC2@DAO3?UzXCnRZ5^{;_J?I}o_STS@Z|;uxl?8J zw&>{j362oKft3+>>(50NZ>7ZRY`m(OqFrTlYZl+CW1@gww-yw6C9Ma34e32MCd=MY zEgJ1yMHfGo4R7->Y!0jb#WzxcEkrv!)X_(js#&6ZJ2;~+iH#3uR^v|-1`$4Iscy~` zbU^Rp>sOSoerE7!*p(>290vXpV zBTbLbC!_p4iSem|UMXRH13DRyO-lnq$PxF3xHMw?jC^VSF9_Uz*JeHtcbt7m0h!5o zfLVFK7h@}x%0$W#lPnX9Aj_|dAdf_lV-Y5=zAnPVNQ8-4gsT(t-^{-R<7W)#D+mAp Dc!l0o diff --git a/docs/public/search/fragment/zh-cn_6c7424a.pf_fragment b/docs/public/search/fragment/zh-cn_6c7424a.pf_fragment deleted file mode 100644 index 9845bd8fe94b3277e093f3f615fa2020669b2494..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3236 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 W%L4YT!J)_heEeVgOs-AAB>(`IZ$nf7 diff --git a/docs/public/search/fragment/zh-cn_6f5fa08.pf_fragment b/docs/public/search/fragment/zh-cn_6f5fa08.pf_fragment deleted file mode 100644 index 88a23dcef4d57f613b4ed2af59f0cdf79b51263d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3623 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 diff --git a/docs/public/search/fragment/zh-cn_73db1ea.pf_fragment b/docs/public/search/fragment/zh-cn_73db1ea.pf_fragment deleted file mode 100644 index f8d6712a3353731a183d07b824aa631c8e2ee6dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 662 zcmV;H0%`ppiwFP!00002|BX}KZW2Kheihv-O~6u&UT|Z)>7~8sg)zo7%kCf>w=CIR zs)mp(q(A(&7z?FN6OiaAknQy*xzB%X2*%RhpqJR_< zH$_2=auHSLeB4SzL#kWgHKeU@E>2KFItuC3Kt497)yxkgW7jV24|NL2Fh4zu-=b)G zQ?y3!fND1!4`50E8mG@F!0r58#|(%0$q*b#ai7#af$i9|>m_C@^sICOKYUv{g=6roqWR$&u;>uAh=Z z5v%`5+_aP>FkZ)hS}Q&6y^)#06Uuq z0C(sI`hdslBm@<3ejeqJjJBMq6Qyl>vm{cei*8L4k)K!H)EJ?(j^2O|MY1CBx}=0S wpC{&8)Q9*bJrcmn?XdqL=V#)uDp6vtai-K6(Ix$=H})d-3&&vC3s?jI0DsOvXaE2J diff --git a/docs/public/search/fragment/zh-cn_761aead.pf_fragment b/docs/public/search/fragment/zh-cn_761aead.pf_fragment deleted file mode 100644 index d4e83f19daec8482cf9ef4f7fbc9c8195b39ac30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12527 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 F004Yii%kFk diff --git a/docs/public/search/fragment/zh-cn_79fa3b7.pf_fragment b/docs/public/search/fragment/zh-cn_79fa3b7.pf_fragment deleted file mode 100644 index bb97ab6ce32404f2f91254585b2676d8fbde5746..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 595 zcmV-Z0<8TXiwFP!00002|BX{!ZxTTi{VTdpnoy}UHjx+KeDuW!V~lB*ok2D(ELjFM zAtYNO%9j))(k9XrutfY|QG-&J2L6k7hUH&)@2=otW8%Z^J#+3kbMJg5FWTQ0haCXIuFYG!7XZyq z8_x74QvG93fvgx!c&P<|_iHPko7k(J*(el^=c4@r7xmi{AZP~ZGRPTVq-be8H`pMuh`1|S20m&IA$KZc{79)!IV|ujiqnW=h03U6iuet$w<^HT*?8y@=~ hc6n(|8Y&PgkN1weN3`a@8p__!{sN=38M6KZ003N%6{`RM diff --git a/docs/public/search/fragment/zh-cn_85334db.pf_fragment b/docs/public/search/fragment/zh-cn_85334db.pf_fragment deleted file mode 100644 index f97bd978dc44ce28a51b3226a586d0b2dd8c4b3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2276 zcmVG)VY7JuN+XC?oLKNy!|x!`~DVr(LiQ-rJ%^xyTz_OZT4>C)LfXRgVGaRJ|TlXx{R-n+l7~Oa9O~Ii_Wt8qWxxxjyUrB z>GCOgYUhRax(Y9iWy%5X({M~tFnFC!c!r3#d&it}oc%G_nUhmklEY4bk zD6rs&&@PtE!jkvI?Tu@Ea^+=iJbg^h^5T(MJf%Fpe@xs*ckYzwdgU#7{#cl1ik;&Z zi|mX~@j{iZRGRuuovk#xd1|d?eSQ@<5VvX<%#Yf|0bW^PcD+)WgfcKQuZy_(nyxI) z;rThQ!`j8NzIo!c1kuR%>&-q-o?n0U#b;lm>?F!cD3@cARjHfx8r`{aOrI~C_Wlx> z5gerNsO>^Wytn~XE*fBekZ5!G6-)zv)pHWI?E8i_S zNzLY!BZw_rIq4A1veuP|Pgcy+Q~c`K$uoUx*Hk-fY+i3$>F64QNn1X^vj@JOCWO4R zthKrXp1}uOEIeP4+Qm|APG4tyw`ff>XIgY|W`#bNU+T{ffJ)!}K&`O&);L~d-0FDY z$Ys7V@6tYc3Tb<*f!(4N>MmMZ>j~J#&9cMq=#yE&n5lKuExmToezV_M109>2E#g4% zd&zt?Ytv}wq6ai$Gy)!ZUYxcSKr+X_k`Xc?nrND0F6J zNkUl$k+oqDWh8kjMLMFfNluV5Nr4-K4W2idtUM{k zQMPYNNHKZ1{C`qNZ#xLdDx-Pbvt$Dxn{`%uY9)L%>*O}&`!^<;jz0f~&jEFoLW-c| zAu?-}qpnhC;KC&nysbYw1r(z@KARLoNrfmOxdIUamHMS2! zYkYkJs>Gc;R&|^!vqJj;z}-^?e4PROgfw?fTX7>YT^m-cw#Y$QLoML)hC_(Un_A>t$1>e!MkPmeAZZ<^VVM{XZu`ebwJfE zX?79Fr|mK@zuVr~hm?ZUaZ9@0x>-4QyG%Q!-tL*#tOMOY#3}=W+H0+`+Z!~^!@2g} zn!d4P964>aG2Ot$3n&YCpEZ)~HgF5O}*Zf=BFOcD2yWd^|`!JtTRQb;3()Xi_F`UFYL2vZb< zgSm+*NyttF*`q&5l?`Unh!< zfk1z3ATg8}4D+0j=p&@RjY%RY(SMDP=23Vk921740YQj_1N`qu(f?y2E2l;?1o9D^ zCXqOvR7QC|%%&otiiS~i5JiXBf}9n{#q^i3iwkq!)o;YOk{snCp@?TuknT-o&}fv5 zvQun5D=4Bo8iKK`&{=x4pG|W~`Tjj>!O?^uJfxSY1$D^p~D z+MX&xZi1e*e=rg0=g2W5gNP6Cp@a~K#&|9;7(skk=pT-83E0oXvEhBD{{SDc9X2LN z^fLe(Q&akhqlI)VDO0ci>wzChCC-|7Jwa`pbrI>rwNBB)10N9`1`|PB1noqZ#4wTAbDJwD^igB-0RLbs0RApPJT+#o yZLXj&AGs}f01u&cfWF~|DFfb)!6Uqj?uJQyH|RXtLPb$e>06aWC3KwyUe diff --git a/docs/public/search/fragment/zh-cn_85e5c23.pf_fragment b/docs/public/search/fragment/zh-cn_85e5c23.pf_fragment deleted file mode 100644 index c09c1d277d0543a6b566fd8e3f8784b039444525..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 583 zcmV-N0=WGjiwFP!00002|BX{$ZxTTe|0;SfKqHP<$^w3eV;cM`%C+%|~mBr?QdaouJ_u!I)VbZ!Y|2?z|fyQ9|HukzZs{bT&D z10e7lqn)Sc0F8b&!{H00db?2svccY=tnUDjy~b3h9M2oaJ_ZKOm7x6|eQ&&RXG{5B z55jr{LG7j7_zK_>(sJ9E{s*KtyRzDy(02;9VJ+R{Tqx~oQ<1k9ZcHe-u`*J+<9&bj z7^PldjAn@baI(Kk95fl*D2<+>lW24fHX(DMNNE>}*n1odx0ulC<8-`lfU zK#7G_%3*(R8WSa(%Ku3Q?F-9Qtmf?i+Keu5T{fQq2>abx;oq`?R#`UGC62t{)j`yi zWtPt{8}ZycEHOl>Wa=`QG-5(2nv83h3J^;f$_gfbg6INDuU$zZna!fPhz|tA_=wwd zQRgnsJ2059CagN1_Ci(@@ V7dWM#OO+m!{s7S`rt`=H005h47@+_F diff --git a/docs/public/search/fragment/zh-cn_87a6708.pf_fragment b/docs/public/search/fragment/zh-cn_87a6708.pf_fragment deleted file mode 100644 index 91183acb010ae0dee4c5ab66a401f18711ec73eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4560 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 diff --git a/docs/public/search/fragment/zh-cn_8a1bae2.pf_fragment b/docs/public/search/fragment/zh-cn_8a1bae2.pf_fragment deleted file mode 100644 index 713355d31150982bd83aa6d1838a3ec48e2ad1b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1292 zcmV+n1@rnJiwFP!00002|D{%MZ`))L|0>1s0;I-C)9z0z7y_h8yxCrWgb+fLV?Sxk z9NV&;c2gB8D|Ab;rUWQuW$F6AjIyTu>1dj)?RSCLabEEixO<*Bev%~{XfG1qbI;xH z&Ufd#Go@*gRCVQsA}e!3#?pi-A*vX%9d$B}VX2a4M-!4wBC9ilq`;H~erD);}&-EVI`ZLe(s;nmBnjXNViwVti`XWt;}V24Q{ z*E?>w#SI|bgK}4oUVL3{)=|(~x#w+e_x+JyoJXSYom=`92oc#IQ5W9_ zuc%Odg)*;SeJ^0SrF*UVPe3$lbv&-uU3#S2ZlMG=y|v9|eP&j~;%TcMnenC@w$t^2b!H(C^a#&~b56j175(uF!-S8*G`!?z8JH?YaxQ zGzGiu{hi<~n~iPA5e>@{Vl9+fr&TW|Hn0uuFQa0iKU2_;xPi9D7rrO8O}es92U4qbhC?v=K}oK+vSR~Pu? z@xbt(9pOE1)*5&=jkI_xtpzT=jV#{rD*+eFT9mAt;ta9CrB1-(VqBc)NJIxq^#It! zf*Xz4wrFK^M@NF!;f4!KdfB zP?H@hozWb%Lo>f4Ed8dTL$B_fAvT(Uafyxu@p#3G`k!SKx)!=_YME(Oe`(1K&aGQM zbc>k5_Wq)Oc&FRkpX(O>m#4H5Zo9P_ujiKjz}wBot(`g#C=Sqiq90$%wz^T&%1d1l+fF226gI;w(egZxC=-8po311$9Z(etI+0I ztGdOXv21A@f3oGDFLhgDYv;^eth14cvW>c5#rFMoZa%c)6@T*%3PY{@zR6nwO>}HN z=N}$-Wtg$9dY>^ERelcpa_iv{j2YUg7u}Vw=r^#-D|X!SDmk~j)H7GE88alEAQsYN zlkhi6M{F#Pt#S(KjH$pbG7^65G~_gxw51~PCQeZNLMA6KOOg!KTbLu*Q)UfIxgi@F zTvg(iFOCXHRfFba3sZC1Q6Wtn2`Ir)9gV;)!;Ns9!`->cmI#Ojt0@6gC0$M#i1XQ; zKs1tO%aEeZfKgQe(t(9ZRY*dzB5;`jwnJ{A3{aZE#-A`1%Z~E;y40f&eE_5)jQbzd zr9c_q!wGfR^29c$9{sWSQ2QTC35{G&e!vzzdx!&L7kU9w@j##7twQBKZU_{Q4+CNw z3|n8gSL)@4KogUFK;r=r+j(H&VcQNX!Vq!TbqIZ--^YnAG!>3;n@u>cISZ`p2k%n7 zmM*>666`n;Sa4v!=|{)yr?dL>AYQsSEG(=IvZlu+s+Y?pYHT9=dG=pTw(DL?3;+OU C0(sE@ diff --git a/docs/public/search/fragment/zh-cn_8f5839d.pf_fragment b/docs/public/search/fragment/zh-cn_8f5839d.pf_fragment deleted file mode 100644 index 7a488dd5ba3306092fa97cce6b5e222b6eedc3dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2118 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 E01u2ac>n+a diff --git a/docs/public/search/fragment/zh-cn_97a797d.pf_fragment b/docs/public/search/fragment/zh-cn_97a797d.pf_fragment deleted file mode 100644 index 7674a631ce4b0713d754dc68ca992c3b4f454576..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4112 zcmV+r5by6FiwFP!00002|HT@6PaDbotGJw0wYu2g2L#g6DtFTLpQFot>Rs8(OvM9|_}~ zncw@po09tFxRQ*06N|>aZyQP{+U~Tq$JA)1T}f(bH8vF06gAl{rK1CiCP%fQwA>y| zNSTa3CMV>63Gez-X(bs|QVF@e?N(b7j4d(Zb8FBZt&+_700 z!HfJey?B5Y0sL5BFYD#kXi+-LUMVtQTT3^GL2i8c#b6TDd12f}LQX#k41KgHtwlPbmJG?C2G z7{-uYs?Y8)B#oun`l|}z|J>Njp~X)ZQ&cXO%kE?P4|rEE&r%=+re4bF=a`Ph=uW-* z9xcq#s$Rg?|CuEMT-oJC42NrHMUZWM?>Uo;z@dPhqGO^21+0|y+zF%HyjpBbPLbV; z`RXw*ac{Zk)n|IX$e^c({*aFgzCp3z;G+~Tl>DMk6^zmGs zTx@J22s6yi0W2IK>UC(LcJ_{EhtlwB#8_K#5J`5C4eB}U+4@A0f#US*0jgG=a2Uh8 z1rRS5dzpAl{=9bi#6yqz-cs$Hh_XCEFI2D|>x)&rRMd-e32%*#>y^WZ5@5twk{r6&wvQlQP=t-M6BpAiXRg{j4D&PX}nZJuC3 zjhLFdLz?=jnv5&`LurzZY-`G`>aU;ZQ}fJw>a&Lsdh5Hp`tovpSKY2d0}HE3!#}I1Cj-?P~%?SsGXI0M`ZG3=gsUCFE_Z}Slnu?t~nw! z5s$HOS>G!2Ci&9^4kVvT59FkeStVX&qH0R^F{?87F6-4j3?f&;Pg802ffAEr1VrAU zD)V4e@IK?IPXo1_ ziK4N)>B$iSJ3m#b+1Lv*m zY^Ba7Q|Bhc{rt}wE*b`19DR8OUUR{FZ!pYhh&a|iv>uuPM6d#vN%3u$6466 z?Qm?pzD|mfXKzHd57$7?w(6K=Fh`g@yhfRV&xi$A{i4iK%&Uc(eKW*PXnO9sxw*sB zTCW@%g)Oc?p$Dk`5x}iOhagZYnD@vYs5XOmjgmiw49;(b6&u3zj-wWYW6W5DFl0`? zVbyOFN8>{?$*t<35BgQ}YLd^rym1RTx-2AR)B-}~VN6YeyD-I5QQyGWc_J8*^@Uf^ z;~0fPi_%F{+;!-v`bd9FLx;1pYrGhvQsBdp6AM%`sPHC~=WXRKR_kLYjwWbn1X2-8 znaHIft~#0>)*j51Xe(n^PLQ6r^{q+b8`Jg5JBK!HcbemC2!vP9F7hsF=dhWl`q`+{ zoQ{AU4sV{dlOP!@p0hg#Rdff+Ws35B-Z;%VW*~GVQh|{X3^zDAPC+}T=iau?dx(FG zdw6&l{qM(7FrlQRhx7-IZ* zm#b2Yr`!7YDQ`kzd*G}S8rfO>a?BBcoE5E$@+F_62%4Poh*dl=S0^#Ws!kSr@R`8} zq&tbpDLEOFlhKEFw6rYKiX8@`$VTFh5o7O{(zK{q62aOu`{S9 zGjt=+12Xq0kQ>qyE10=1OMJHiArBHNnDWl!XkPZBEK+a?o$@-N^#3T1ySMCiZ z6^*VR$O$#7Fs^P$yg;Eve@sR-W?GJYuEu0aIju0Q`{D^7x7So3?JkqDeKJP5m4N2=M`00A!E_e=OzPn=+yJAq6sA$=%ZO4rXjyl zHuv`2y?{?ZVCAuyH^7Zus#|OUQDT=2r3M=y-mo!o&z*#d1Bm|;zg+5fyhfqPg~orH zT-m6Nl?g022?to}q$9}3OJoRDqN7>ophOsA()TLPc>z`O zEh78XZC*%j6QNz>f7?*lB#IWED?0+go~g+iS@53k>TUn?=)!xLXOAl?m2`bR7H;gA z;%*^XOGr(q`ivh#+90_W_uuGmjrW_>Qk?;)URg(g9CI9Q^u;t*yV^5m&mI17a3~yr zhKgFJQx5Ki0g6I40Ji;Va}UjfoRxCkvpGkj`W8i`T};8tjEgcC=JwBx{aMx&8re#{ z@{DPaHLq4rZcP7Rtiyz-1n)^1v+RlO-m~Y8>;}KZI)8ZL^He@J-ZQJaMv+wWTZiT` z;GK8RP}wf6Mj377l^8VaWQO45lbxkjw>n7jdAq>GP@T4x+|wIO3H~=sI64ex`{(Y5 zxM^R}JUQ~Hm|epa+R$5av5feghYUE$v=9g0{@B?YvaXQ5y!hDXX~F*=IbPfFaEyB| zkr$bJQF-0C0V>A-qk3uNH(MG$RA1Kpq9F6APjQm8yIxhHM8F zA)JKRi}{6p>)Ve{(2#`eSznpd&Tu$2PV)Lp!Cg9eylGo0>V-`v59=!ikrw;KqLsbe z5QId<>Sw-SXg&63pB@PxKIk(e#vHuCpzk`&(<4^lKmvmr)g=$(`35ap#*g$Xh$wkF zclfB4RE1#;e+9R%csW6EMz4{6!%37lMbBnxm)A_9&`mV_mvdw=I%Whq^qBU3*&s#b0X2atn~Z?5i4$_)j);Y!@NNUizjpS7mnwVPWI0fY z>*n(Nwdx9}HsDm>b#F(-7jE1Di)9$oyZ;=UTie|~{?yE}WrUHDli`tO%m8Ev=PzE#t)Z=&iD{#-H`4ux*D#gzn_ zHfP%IeE;ZH+n}sTXw{}ET0(BS(*~x1>QyiTG~h-R0N5PlMEK+mKBPn7l|iIjbZ9QbLEUdB|8=w@RzmHmWPf`w(i6EIZVz^KhQl3^ z?(T3`caL9=hCBLhhx+`%u26?RB84P>cULgzk99>ukzmJdDIN<2(Cv?+2}Mq#VNUN* zwFKvwz(wBAstll)ZBtzazPB}u_3d7!2h zRJ{HkPDMI9xQAaWF>RnX+}+JRlM-J&Ov$}{XqA3Qo|87PHwbH0BRQZKz+?v0Z@4Au6}<+~hBCb|HA%O%hv<0H`l(5o`9^gwcwA5n$464)mq_iJx4xz`IJdDqv6&l46Zrkp3Ji5i-PfCvaGWpim z4^MA!U9v9Y^rzW(;DcQF8rN|@7;1|DD;(^@XmXpV!)uR&+7l6=Mg;kgDW*#x)>9i&B;3&y5*gfZFs$F) z?MSfMZJ0%H(z1SYx4VMPWW$;pZ}BwS=3u)lNZ`vKY{^+qIMkXm{=NrqJ-54>>YYqi zaH=A66;A1{4Gvy+S5taOR|(Cer(mE4+ZEmj*hh}t+ml!zi$ zN2ezo;gAGF;pPrQE|LC`cNND|2dLZEfD%Y-PE{z>-5M1BY+z}O)xjc6_cjka-1F$~ OkNyRIB?zx?G5`R0g!h90 diff --git a/docs/public/search/fragment/zh-cn_9cb1588.pf_fragment b/docs/public/search/fragment/zh-cn_9cb1588.pf_fragment deleted file mode 100644 index 0f8d41aa76c2f7ba52695c2ebd3ba46ec72e7007..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 950 zcmV;n14;ZJiwFP!00002|BX~_Z`?!>{wrW41X6r{t?vb$_=eyMU#L_egrc=~eYc9w zw(K?O5k~O_@`W!9oE)EWY16+q(B>@7`qRkIA>Y0E*$p+4ilM0G&PC9G`rFs)NTR z1GUAIQQqGMkRNPJ{iNaZjpsuo7MrWZ&LgyaJ(%o1l(w&q#{CrpYY+0zegROC>h0tF z^b-KjM~@&V#vDB;zPo+3ebO71IptgX$S99YSsX;&4jgIz^6-^b1cbkOx2CiNXxt+y z`)}G6#!KxyR6a;c+uzUDF|F10^XhG_!8$(t;cWCNiofqqR{Qc7E12$Ej8T_(3l|5x zK~mgAo69|hmgC-LLF{J%pf^!`pGoVx5w7{kT@3is!~D+CY_aphrzo!MPqWC62YLTT z058j0{=7Rry^pm#9%7*%pXBSqvTev64abAQWuj%2qW^1guy$pX-*#rBfF~xl(i znR3ctQ}=%bEADQNdv`FKZ;p!lduST0=bN`>3HN3l&(kWldijPt#Oa{;@+qp`W3gZz zmnA5eCM81r+12Lf#H+(y|zXl)kJ>PM!IRIbBX27FZj1mg1&+uK;%{j&AfnZ6h zG?td3~)v7A>UEMHTS9NXMQf=L!s_VF>sxy<; z>a^|=t5yNmSG<73^?!#pv$|y&Ot*>3Jl#<3S4k&+!%w14GbX8EQfxEcYKvyovZTnt z%eHK4*}fwcQNmk1e7j^Bwlk}}!Kr9BP1BlHlHhtbW=$7R(v|vzz;tSwC5{A2VN;i4 z+Kv{lV;!453DPp7lz+^cRok)cYAJ(9mNsb=p4&wi_~C^pNV@dpWkKcdGGnJ{R*9#-(2t6k6o=Q?S}CjzxmzQ zym8-%HXIxad=>BqzU`QdggTyfbOplxiLT&SG!hO>`lG?{82%BMoQOtVbq@Lb&qpHR z$+19J$Kwuvcr2=oMe!MZ_sG~<{64nW+`nmVAK-yr${Kr9kMPK-teUkqWa|77c92>9 zu8fj3p{v2rp^7OMlnYQ zGfVnLfls4ao^0Jpq2xZnBR4C6i7llSW95|W+PT$t$aV^j2oIeaGoku!HkD3xhcw+Q6(1(hiP zxlt^Ue=AeKRU(nvCb_Xj&__pAfs+0uG(xCg3#6cPLh&ND}cjs$`E@II?hylW!)j8s((q!$#GZy=`SkqZn(HHrr5lze`28 z;G`n02le;?{*MfD*Uha@CPE|>wyH7@$;#^w;A?r(1M){n~#IN+LQ+hB* zHE_3N?8nS}Ow_zy(`sOi}aDGXj&oVlS zXXMaUOuw9#44YX(S*qpvo#sZoQA`ScG`Dw9u}JaANq*F}&6`!BK}LBND>vEm9VPw# z3S}+?4}`Ly?5uH;!vE@fcLJ0-Q$@*5$+pZH;B3}6faQXgM){~wT$GBoYoSQukSH1H z3sji~w9SJLDiD2k7x)K4fJ>V*RjS>DKvcL__ZxdFSj(semC&1YRc*2Eqr(%MO!URL zeI}38@)B%>F#C+|xxpqwk^I3iCBj`AlA4eLq$+#?&!Tl8Q9fV)cQcADIUCl&_ zFA9{=QXFMJNFR}%1r7E|znnQjrQP4z+kO zwt-?LV3>(Bv-H8H3_|omD==r&7-Tba78E$Uob21ozo&{{2kqvWf?exd-GInwINuzN zS>L=?69lnSgm0V|H_Flq^_eRs;`Av>p8iN6tAA+5cH3eo1WMJLhF2U?^E)pXFC_e}_3Dj7R-K3}QPQ<4HF>LqHcl<8nK#DDuC&?XX~tLTPMf)mu^m$n z>TfTa38hpz*kK9~TT*{Al5q|}vmC4jfR%)E{*ECa%!HHnLuFo?2}e&L{6l4-6BBE_&aIB?GD_#3Y=eEW&uI zw>8(xXn&_AuNSGht?C!hZ5chvtc%Ok%(GD}vqY!5jO|={4QnWsaBQYRy~B^)8-77Mfn|Wc8O9@Ud@r@<*p-FZ2aJCq#0I=LWvTQiPGA{kbM%XFJ_Dv zu9JrW=CjhL#!0!g5X1!HmXW1Zwm1hfDmoCfaFuQ=feH&x+`Bg|&ad<-)HH(>WaT_NEJXm)M6p2NZ_1!)3yr;qsFSV{`?L8l^X6#$k=6|=s{1Pe-*O)LIm+KiEC>KdRV=1@)Mw&>yw z)(m+g~W7A@87y$g;H-mH}8F^nFWJY5962MDYN$x+`{d28O>saj;@_aV{2wY=I= z615}<1~=xR(BR?ac)C%gZyMQoI4^v=O~u=({X?fuuyM`XEW(UU5#`I(9Lt#p;Ba2) z#kjt4;Uqhj@>tpwB|wrX@%cKp#H3kwv=&vTSvlsc(}D^HD!q#~_e5m`iY=2@zFD-# zI3Yw^;GnHh0O_cRoW|6Reys#*=$TnXBN)hlTm2h9cnC35eaWFT9*YoxwGlFAt}yMM zOT=Y>iY(o~TT=0sKJLMrY~5{s$VPQ*9>GegElbQ7$pupna54_Ol0`0$r9ez$VVw0_b+`E;pk|P{vG#?P|X?DLgT}eAw2p$`T0+cuKpwZNEAAl zeiWz8A^zD3SgSsLDs8KnM@dr5{(?5EKOUHM&=1lSF%D~JsRZ3!vbS6^^LaIN(mW=4 zsl(G`+gm>EtG9sXZBEC`!kyRymx<%H%S0h;Y+hqwLmv~ZQda_BQ}BZfEqyD-Ug_$2 ztQ2JGnLipH(jv5eC0)bEWaR30kz)2K8BdM}d{JO)_yzs@8~p!pe{au_+ckuTza`S> zc---II1>2EAD$$i2=(*~^gZqv4u+yyWTNBgx33>}ywIXPJnD!Bqam&1X$N>U_=X3c z4!*#`!_%YD==j9bF8W21Z%m7hN5bE{dg2QO$9=D!_`@%{CZ4|v`6AD|=s(?E1l6wb zv5~GGkJr=h?&|6D_I3~SxduG_-R{mFx6j+%*VohO?RWQfdICOQr>|f0cLs+2Zl7zw z?{f_Wp5W8J^oN4lSoE)f!AZ^AtM&Kx`#RnJKwqcl*S;6yzZ;H(Ukr}>CZZbsIjRLm zMx%qSzHa(6h))d-d;G%#Y$6;9js(a40=v8(mooWfFc2Lb?CEz8D3iX>7q7;(!67_~ zyrT0FUo;pVgoBZAl>8bS1mF{+;jh0SJRBVMg(d)@iQs>vq7;9)e-IS~wKL%v8S zUYx|!QSBSD2aiHwKP9*0Y4_{L>iIFB<-A5eR_lb>pK#pA50lsvm1p29Gx)^Ja=ypq zc>fo^p^)}$_>b^K+=K%HJ-t={qb`0P=S2`uu|ov!DM7SdQ-BWGfVu@xCFa0~MGDY% zYYNbAj}5AaLxm4u;EN>eH6_T_1r=VuBfNSRXV&5i7X@C+f&#DKCd|=ZK{b7#fLIFg z1j4mKyYmitgUj9b@C_;l+uzW3hrGeQ+|<>&OTEbJ zD8R!Ukq1Q=-Y^vS?bZ~iz5R|*X?tJv_DQpYcJ-}1YT1NSL3N*U)qtbT;KEIzJEwqe zS&;B>2y{Z-QKSz^vd`;igHSU=!3>8b$hK<|YJZQ{iE40uq8+P%Y*|p?c|499O*>f7 zc7m!}xLSpy4{nn83_PIMiUS7_*hawp;A#wd8PMr~t^qD7aLNqLg7#tHXF!pddYvN% z)UQyyLgfl&Yn!O_Q?{!9-{o?;6!JbOLCMEdwB}V*&|%pE-23|9um1{CJ-2A?|{uBVay42XX+W}Ccw(Ok# ziwwQkW-G|3{rcD{Z2(}sSmJneVsWWnB@6B4X?t^v%zeAxe7ZvC9=>u)lLVOk#+p9_ zAW4R;?fJ%oJpw$bTGd@LKljX@d2T)0^OiUVv-Z&>NAT9wD=)~qwn_%~SL<(P32-`N zZLgAbL%NDG2E~|}W_rD?&A|JHs!2+&Bf{xzZT4-gObDJ(%@z-wuTKb2Dcft0LxRo_ z;qARmYyDd;hKGCKUO5QPTK_HwWp1qA>j;TZuaxQ)VlIM2QJbp|8qbc|T29zHJRfaw z*)-K2T>c7HvQo~}1Vh3K-OQJJpcG}WZtc!lui>3 zM7?1f#w6w{aXQVlqVw!7(=FtI;K{ppx%WrBqd85v*B-LXX*{l2Q!j~}r|Zu5%U)bi ztyD8DOBn*uRFV}kh;D|WV6q1{kXDe`&^URGrf<70D&ipxn~DhFAL02#!x@A`2!M~U zqut0tsmu0sI~rmqheGXSZPLgW>C*OZWo*7LLc|mT@jADOv22qz1BTMMyZoVD;_Bi6>RY-9^0Q z$=!+|r$h46%u;M(^VnK~>GT@5PpZsctp+AFjyP`mi@UPglA*I~ZA zwBgBl1?N?)Ahzd`7;G7RjF2Dq=h)AczT15QT9hMNSZ7nT8o!Kd0YJK)5>Xwv3az8w z7x99)^%st}+|OT^GyE8Dkp^00zvqcTgC=7d!MGsf0-;2UzB`k_`J^CAs_<1(hR~L= zlFKVO2sfmIq3FtFzK}@<`rl;G1a2_4N_7P+iBVA_a$VxKo8ZO|=A4+JqWEoC|E5Vy zLx!m-Z8jkpVHlzuD!`E})bD4ZaKbsKRijnejo{d_=G1)SWS>`G78Q1>6iRSjNp`b~ zKppyaN3h3hvPM3TIl?`-n>yO2pmy>_n}W7|SDANKVa6W`w8rsXi<7D!@2~*E1xZBI z2ciG7YKyE_bI!%zn7#`d`5Mceg+!wgv(x4t}un4i~Hqv{z2yxWao|?o@#Q z?Fb&@+<-YZ>e_ou*z^1*bx2jgWSkh8{19rEW?V-)HZ@*RXwIWvsS3szz<+{gkgyxR zIRu|Bf_QDjEvP!B@BWaLNqD}@%c11y^NO7hU8V&=HW9C3<875oQo+bBoYVeZrME3a zXWh^QHKO=<7|U;My_%92Bu}gxhO6D@@O(kDn3mH+Zo~{6;BncMWRbPVX&-z);Arho zT&C^C=h{>D#L{!J2q01Gx(>4&UkN>r7-N z8QLm6F*rWanaCqU04QNdhK%55kzJXwV*tNvOTa=w$&6uw_4#-LvC|?f7o`HQkVF6t zu}g=_mlm`xm^5M7KwpwI0LhqnUt+LlqBDrE2qAokYFO39Mt=di;31R1F?<|$amj8i zL-@UYZTa6M{nlvq3cF<0+X&D*5GF9z>*2F2Palo{2aw-wi%jqK0?6LKkNi$=1Ucm) zvvSeZ8_=H>kY1=iVF7x2E+EnuBhnWm(x1A3NGe7o6(!P_x=5Y=7?J)ck<^7Y8Hf=X Wh!W`^cz5E9iJt+;dFxiK7XSc%zAVcC diff --git a/docs/public/search/fragment/zh-cn_a99b2a9.pf_fragment b/docs/public/search/fragment/zh-cn_a99b2a9.pf_fragment deleted file mode 100644 index 262c951b78306473b650834660bfc6423529cc42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3896 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 GDF6Ua*Ms2z diff --git a/docs/public/search/fragment/zh-cn_a9a4dde.pf_fragment b/docs/public/search/fragment/zh-cn_a9a4dde.pf_fragment deleted file mode 100644 index 72858b5672c0c8402ff90c22a3235d7950a9cd69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1599 zcmV-F2Eh3riwFP!00002|LquEZ`)M#uY~XbNNxJTfGsZwijWXuOmu({Le-6Z)0jH8 zWjlpzs+6ToztXN9>$1^iqwU&K#+nxB);9h6FA&?w6Muno?zNqdCZW|P@xnvmbI(2J z-1B+Qu{APCqmm*Hh=O=Cn9${5IM^ksg3)CrOjVbNY;?s{F(K1VRiipFB~|eeiYzI# zlPF@R8jT{{73>HKs$x>b1UdFk>rOuVXDVA+c~*J23V>alcb0Dj0CJ9U<&!&z+FIij zh_&|)t?6X|tgU&Uk8RA&mx@Se=O*lx$GCUDP+5Jz_U`SLr_%^b-L+)YMy}aEthsFj3N!ZPeABc4KAOShN!Hp(v*jX+7)nH_V9E#* zO_P%dbiyx+fT2c-yEdpkOm&$W1}wVZ$K&MgVqA1pmZ|RU0wO96x(kEEHQ;QE0umB2VwN@?IJLqu?hzWY@{J4tKlY^ z^aa%oC^T4v19gE75n0kmG9;*RH&otSBE8$D?ePHg0I1^C)6G4MHms#D!jdzX=(_&p6Y76idf(@su)mk z0u8A?QcyfYVni8?5ed^fnNS3yCc*0*3`IBuG4$>!NU#Xo>pJ5b&lY#->5eebJ*P#a z8=-FrvsVZINdob!p*O&_<}I(^geM*JqyB1uw=QdK3@e!3>*a|j{LW#RJJ$XvB2V!) zaSpNKovoa6Z=E?$8`OzNuc>gPFO$`aEMJdN<#WYdun18q_TgQv$30S0Pq3s(81#Kw zt>-9oYswSI6k>g9g5_D&97BjvaHgaTUeuoQ&Zt$~sI#Fetlo@%ftV)Gp=NiGJ(aTa z%tc$+`s~6p|3GUlT~3X*Zl@mv8raG|Kdwwn^Gi9vZ&LeF9yc~_H5lOc+BT74SMVef zZ!ho$Y)y_khhyvj6zKYzFYLv%bt})9rx@pyZ%8FG>l_!FCJN~afM<%yu1p{ z)>Y_-(4-=SO(U!{za$20R56J?HE7ywRG_jJP2lA4E-t^KZ1<|l6g@Zj~n34a|7>Sl15SKh9j+o`i+JX0Gkw|03sR3|#V<`bN2M?S?RIa8B%{wWH7 z9jB(tSFT)or~5L1Z(wBM_5M3m7Y76!ErQ`oZ+CYDqmm5m-Ux$w_D>akTpz1{yNDBDy}_Kct_mqxxB`3C^qS_l^s000){71#g( diff --git a/docs/public/search/fragment/zh-cn_abb59bd.pf_fragment b/docs/public/search/fragment/zh-cn_abb59bd.pf_fragment deleted file mode 100644 index b55a3eb9ad68157d4b99a9d388a70a16ed794115..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 599 zcmV-d0;v5TiwFP!00002|BX{oZxTTe{wsPWtZb$II~v}nwa$AZoZxQzMGl7nT)zZle$Tk ziAHjYV;f3bi4aT6Ml{Q0ww`d9Zkgc~M<>+KO`46U_NqZyLP6n#A(Ey2}{#-QQW9+pz zY3<0^cF!*t5R_i>7heEeLH8|LktCE|(df>L&k}Zjh}|{c8~R=oj-!4Bos~APB6;SQ zD*mA``n8Rqy2bm?7C$sD6-DDZ3N*`M`S=KBE2lYlQq$G8NQ9G-|bNP^xrc4E-Fr66`2Kn>{2XhousQ^ti zwUmXL2RVfrG#yMm(I3J0bpnu)a27Tsq1s^xI^bsX3331#mZstoDe^C2N1pieXtbnJ7>sh5A*+b-rA?@}XPNj>bPNuXn@@ZCY(>f;6iB0!;2ZOTPJM`nUTVVw(?zG$U zq0QP2WbZ#%>%#kz-^zuZgZJHZ+9FGj4RX$Dd9@9$amL%uco)EY^1z8%q> z{%X^-caCoUl+UEdOLuAMD}c-V@7hgwwuhY#N2^1G8DQ>sqe=m*EJ_Nq=5`BARzPW~_H;aC`fS$Fl*jP8y$u8n@zJ%`$xApA{ zs=U|!QP`$2LiD(*O?B6I)D#*vX-J!dn}c?e&}71)fUe>4E|JljmMo(&kLf z%Dq`YiG^iczj73Ef@Gomk7Up*-_2q*uc~5cSGKFbOOLU=mEB(9ud<-6yeL1S>50{2 zc}qx=k?DAFw$UiOA`%TnFN298R&^Z>jv(3#@Q{R-7>QpU0eBG)119jHW$WXb<=_ww z4_}EW34=0Xr&64v+0X$p zeINw`A6M-G2s+?q<5E? TMn4=)-%tMpwOaczzytsQ>^xAq diff --git a/docs/public/search/fragment/zh-cn_adf5184.pf_fragment b/docs/public/search/fragment/zh-cn_adf5184.pf_fragment deleted file mode 100644 index 69a0cad6b2b58562926c3fd672acba140866c682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 936 zcmV;Z16TYXiwFP!00002|BY2!ZxUM+{wwFq(>V!PK^sk9j4|<1A2cz>7&FXl+KJN{ zGBZ?@rpZWJEYOyE120seLW>uu7iHdi_u5MaT1&zMv)8P>zO}wtHxlAB zi7T3XN0#JyA!(?>lrSOdk~uM_$V3lYhNx(YHa#JP1WDH{qFKzsc~f&sxgU1UuYB`2 zHksgb*4^@hf0*R%7rcWUg@;b%E!-8G{^oOj z_pIe*Qy^rX(A7^&I0b5NJMC{ia-O|#>$$$}+SSV!uMuO87%DMM4z5?|MiGS4M_K|f z-M48TnDxG|@n_cU9FFiuKjpnvi@yE9|*w56;MH~e%h&%Gez59X`UzO@d~&oKgBdTm9+DE7xc{)R*Dgs zavC-NeHEk#`~BK~c`}Qyb(&tmBCuZJjj%<_P+YIIMe}=K&x+GYN_0b1YA_?0$Yt#IG`dlz@<`r5D$@9-5}sttVnowC1k^rxpf z+w{o;-Yz+}yNY24k$8Y1Zmc129Fh(YuO85q z4o)lBb~|3PIUtHroa|?(kr^4~OJz6;LjtjsjctoI7%YGv3+5~rJR644ebsW-YS@>w zuo#R>0PHtX$;QI=4x7%?HfCjx%*Jpqqi5M8#HG{GGyOSFCS=hfIJgqe+lKdaL%t*FNmz)H$@3v0uBa9hwZr7_W&( z@I(TuFlxtui+ymyP?#dX;2y>$d;}kfj*Nd@jH%>m{1X0Wm?H>8CXYIpiT3fP9v97E z=EAtmah2m9X9I^D{77npFvn(geEbl(b7PXj1zk}!lA2>)f5Za1IyFYK!-_hDlbB8_IGQ9iW& zs_MO0uio9&3hKuhLE`&(j=$l{tD>*P7vN=13+Q=WRs|$#ft<|eMeOJCAQojMhb29r z;=G3aX#_t!(vc?TRSpMyjXqA6bU*+u=A&n&<-7kW+&x|W@pNSk0JAt>dVI41pwi)j z_3j%owfU4`kXiH1fid+M0Aq9Bp`#0P^Cv}uXfE6~SD%oz`&*}Li*)VVU2AHT0C)an z%{H#QgE z8i#WL{O@p@P%TrOja6giKB3y*B$GQw#@x0wcJy93%1q|hjmZaIKE~V(5R!9_ZIq5) zQJyae|HCEksxkQx1P0*0g`0$YZ_+B}vf151pWMn;mV!r;? z(R!u+N=xH}@xc@D6IY$L9#HsuLeM#!M!KMVE=w7qpSse^1A;U28dUB`Y8^i?AHN|~ z#k0d$73Fx7s{J^RL(TcCE`sS=8i+gljh zZ>^WBj+mwKJ!5{B?D%@axJ72n$@wa!+T@gIiHtsjhbb|y!r5fWp6o=5g4G_5B42`s z1o<_@4M3ux8c30!zxm*YlXFc-6cjYvq+xXst0JhRiAN$lvW?V8(6wrHeStHTymG?? z=WDUNZcl$=i`6cnw1|6EBx!&Fj5?775|Ly?P?UUH6gUELBGR;)o^pfy1jy6Cv}KO7 zKwhHmHN)&?Os*Ku?;6t!bg}?$SHl#SA?NkboadOK95H9Y%Ub(^(2Y?~p4qpKjvP0G zU@{61Tz{){!ipfKOiu3=%9{P~;54vhIUbP{&U_!ZKapR+40rQG*rv8ZJ7WVb(( z;h$OC(IwO7>>Vw2KP(y(yWZEbE(yvp;=j zOc&V=uJ^{(ZUN~o=G04b^G@AeUafj}0ZJm+Xu;ZkqDav0w!*=11pZnGoLBgA3eL(b zbrFMS60uM^5>A&5*#sBlL$b>EadMvAk;kLaMqfq{!HqOu%Z-snUk>XCAfGPiB8E$J zc0%(@D^&2%UL+gJqD#zojVe1+&2q(3pNWGL>L{7T^UY$_a#hCP!=>>XBc zDh-f2Oy_CmoeD8n)@gvJf^?{5<)L1p>{JFmb+8RJ;U72^X^zAqF*XK@yq0ngpy|Uv zAjuNLhwmQ)L>VX=jNouQSyn?E+5Z(th3~@j7RFSlryL{L;Ib*2D?ZoG4l73 N{{zU94tJ3o005G@J^BCu diff --git a/docs/public/search/fragment/zh-cn_ba91d81.pf_fragment b/docs/public/search/fragment/zh-cn_ba91d81.pf_fragment deleted file mode 100644 index 20a9d9738861276fd9f0c8e1a7e6ebac50c7ff5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1419 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 diff --git a/docs/public/search/fragment/zh-cn_c2b5e49.pf_fragment b/docs/public/search/fragment/zh-cn_c2b5e49.pf_fragment deleted file mode 100644 index 1c484562b6e92a2939b0a8c923b66e22777bc10f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 728 zcmV;}0w?_+iwFP!00002|BX~lPZLoT{VO^vOn5D0W3W^C)rH~NTbm6@x)@GZGn)M-qGqu$gP;2 zTAAmTTTV6&VD3GcKVaYAL;x@DCGgfXXb>(i!l@Ce4G-UYl{Vot0Mm z{FrNMG;?C_HM?UxeKImUtc~-IX1{idCfJH~ChF}L*NtF|!lE?Z{eLDVAn0QZs8@qln#QQrc(1J;KR27ru2Al!uwVmJA0V2pZd=Cag%*H{ov#gCI+q}z0JsN)H74~%ZXl8T+Y*$h?(A{(T z+`e;8pYAM5N@KDrekTgzrGTj^fs=vbA{O-H0#*%8PMC&_)&8U@i;^O%l73vniqwx( zu|KQf45nX?2aW|OwWJ!b+gjahJX-wU^y2>7fA=4+!@#N*8&9tE!Km?O#eR2#E^Tjc z3c6~&u91Z&Fd*AScOUz(T&!0qp|x_|T6;>r-7W2}-(%nI?AQx4bTEI5EWLoiXH3Gz zJ5s2^zgT!kvG%e0ruq?xpjr^uFW=TOw3A!yjh09Z*K~YoFQ!- zM!!x|Iv6XG;Z8@fXe!b$#RV&hr1|NF4TX87yjFmIQ-NN0AEgnSsFS^X-yg{Hf;^B&!DeILu~R@!fe^uhl$!A79fezHh2nY=E6pb1+x948(U5sFkl)FcS-ej z8`6DUu$pOIV}>tog5j<$EosWNch|veva#e@TcYl5yuHEA=x$ysE7v4<{kJ!Hh9+f8 z3Uk)t&wetyJWEPzjErG(v*7nKS-xSfZGq6n^C$jbaDs-sz17!gFDVtQe3AJxPbywk zc&fRGbXa4*`VZMyr7If`Tp-AcmrR#(y+#pl+!7T%3iBA-`TSst@_;nMv=KqFU<>A8ntcLC5@l1kn2@@XRT2v`Ad&y zGWTtxwtS$++=z2scTj0-XXBUv%gs3B)^sv()pu2{?=phryd3rl6|ZbFGJ9_uD5;>*TBCTaXTaPn z^tFel7ew3hhX+y1`TbezO7~zV4(RM9xd2jJB1?DtJt*%D zKkM$boQYKWBgiV9Didj0O@^CakJw-N+bgy&sbo+#>q+UT&QobqNu*3lqe zjT|%al#&}tI8nm;f^a+tzyHiyz_mha-{4Hat3F$KwONIBfi5BisS>oWi$T zCdVY1kpo!C5tK8TG_Vf{aFls+1Sv97zk)6wOg{}jbWz0#EaqgCVO~M1EHE9=R7%fC zI%fBhN%I113=Red>FhM32*@LtGtf;485|9UPS9D3iqDy9g2zi*=2NO5oo8fFN68^f zCmP0v4qVbq;_*>X-$F!$_vxS^htkOns@h7DbZ@Q#Ip;V`o-gA^rgs-I7G z^|XE=6bv!PUYtyYLfkntSB6uv$^cF%Ii33+>4!w|kA-TV7XxbIUHTH;aUOQ~o`RV`p{z8eTxO zFGhP7N9)0ib@#~u#jROhJBx`|_FStvum3%)O=3li1cPAXM_br#!TTDh$fgtZ@z_8- zKETLQNK<4fG;l`lZYiKC7|I`x=EvIFN^nY&!~_!D6Z<^Uow~vPWRDS zLZ{zI?CY1T5|DR!{wzV}c3Q40td+24w!CY`Ziy`X8wP)3iu}B8@3AKu-V^&CCn#;L zYL`mBE}luT7G7GcH|Z|rmo>i>-mUJ>OxxwxE)5svZf_Ij-mDrAcdYL>oz7@w*1i0? z8y&T*N7JOpjMyt#w^>a%D;PMzlO-I7U&q)e7>>Z7lk6-MMc!%T6z)eF1e7uXr7{+b zH_?U%%So(>-wD{HZvrzIUIrh(4iPa2V(D! z!Fzzv4Tb1n@K6-|s&~gBI(Xs;IJENVjzsiCq!$ty){F>lGvf1STbt6x@x&3(2_K41 zEExJwbo_?DCqA*@;aTEWzB@j#&{6WFl}~qkVxhqUG6hN)- zwfh6)>~4rc4MDN#A0N-|F`wf$r6i_mk0ckV)jopzS5sPHt!wbGQWradI3F$E-!ura8VxbbK5^D%X4^G_|A~8dBFuDci{Q1 z3*c8Sf0jiQ_eHnx-z;BSvsplih2?X8?_kOal1=4*B@?SvKevL_yzh&02Se@}ui_*4 zz0R!gPg$a#;}v;`6IW~;q3bS9F`A4~8yArX{6Z2fMy`R0B321OgB^HYT0C2vmETun>RvATsIiC(@Hdx4Jk>=R6q*TnL)9GWwQj2t3X^ewSq>Km9}n4+_VWz6`*kfO&ecw`7hMF>pbxnIA>;ey=%vAE8hCx^_es0 zHfPR!b4DX0G%hLPuqcQ(A}L*t3`Dv`RWQ2El&R_xk&W)8DyC%GrD{|srlcw!L!_Fl zrjt}LyNDuo>8dQph%nk6>5K@fVgetG*^joI{QN(e`O}RTrw=!Qu*)mX`*E(bn>9t2p@@cJ-uS-z_2a6WFOdFa;Vlh8@!xyuLF5v9qbFOX` z)}5t2mM{D|vom)7S#b5v>MazTnQ|((g2~`^s>k!z(+M_QMK7bN7!^#}cZft6{NhL$ zYMi)ZgX)kInHmNRy5Yyy=gwkfV5Lyqodrf*8gT~(fyB@=73()?sUAZaeM8=Ufi^2usZFDZJx-pHk1vTkzmFJNa3gC&Sb5=@{)FdJ?i6UvenDzBX(jp&}R8V_4*s_k}ml0*T}6P#vnCa z9g{>J{8+dIKY(9fv?tnwG&0_2C+7LKeQTMQE_`bzzn)GrB;1<{*l0xPYGnyoyUWvt|>rpoiI#in^5Fb0(KKgyi zkO}{E#6Ke`ZA2#`y`qAS&t&i*%fn`ZD#Izo9kP<7R2;<1Nl$vhrk-M_1ni`*N=bT& zn%5{*E)i2m3{g0Cq8|z?o0NefKG4OXo1*+=fx@7gVh6AuxgjJd<4JKdUOI6+|Ya2xsr9b?1$p4?b}Oe zw6-_t3V+!@gnyr2`C^Dwbo7J%F=O+OF8}X zftB44J_M}YlHV=adA7J>!J7LWi1*k|J=?6EEcuY?JUO-&%KS-zvXyeJP^bsWz1VYq z**mi>z5Klq^uqhG(}_%N_rRC&@Y_{yaAwcsYME@XgrO<3a+t;1?p9FIE4O^cT3HGe z)n#>zCS$zB`_93qlTkg49^l>NKCi_3DyFi*4RmkbSDBlk>z0ps0lDkQXM%Sf8vhVM zy8q(E^A~#hf&2(}7G8hXR9zev5Z)pK=X=g|M&gnTC}l(jZj5(Eu>AunVoIh=@rB4D z;l&bxd1%%WDhmH_BSK}G1BUYiVg+`6H2Jr~A*9RClUXx*za5C8zLNCn{l diff --git a/docs/public/search/fragment/zh-cn_c91c9e8.pf_fragment b/docs/public/search/fragment/zh-cn_c91c9e8.pf_fragment deleted file mode 100644 index 465d5001f1f2be4091a78bb22f523ce1e8d429ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1165 zcmV;81akWyiwFP!00002|GidUQ{z+=|0e|ii}hxgA5_9$vGAARII5g z(gjJEc^SMYpeu0090*ayB);sOy=LXhG=RyM(YC<>rgWLxgIcZ#Wm<0_*T4 z(*Ul0a)yg92!RjFt{oMW%2y3gv^SpE)t^ANX|;BrQQ7*jQ=A8)@I79s6Ji9|llSjZ z>3$Xe2thd3g=19*NpbDeuB~;XAH=8Nfme5$YddZA4{7d`Zt86|s4c#3zCHuuyJh%a zZs4T@5Pz)69gobq^Tq_%=UX1u)tf`GUE0QH4cArEsy55t0I~Ho`Tslr7-FvEalPa+ zaj6F7SlDm=dE_!lrcG-Tq$}%qae>;-`{H2WQ%HY^bQKxK(Eou>@}9~f$zPOij;Fxl zBW-E3hg|P=spMSNnNwQsMO;1r!aBu2?7M+F^#$Dc*?q+;C*a|*&|8a}@R;NV$m{dv zZb&#j&o{ejvx1A&{s^wmm!QLqBT`p8{|n_5yplXX>8vW6$X}LTu3xbouC;Y_TIj0~ zIkff?d_cHRvn#K9;`Z0sSBn6E-jdvo!7Sd_wOrPZbr)q-?NOQx34g}@d>LGgJ*d0xcfdKUp~Cf@6$%Zl)!H+T>O;~Z_z+r?K+pM zbWp>DY`(l8bDrHypnKMEEXyk(L$Qm$*>vFCjB-@%Mq$&eHJ^8CC$0_ybIp6e?4=DS zzX;K6AKR;Y^zbg@4cgk;UbkU8EEwKpn#84LuhCI=PcpRxyUjvT@;6MW(NLH?CV&=Y zdio6Wk;>}$Hu-^OB0d_uGYZr@&<}F;f2ry6w4~+W_76p3!(38PO{5###O&NKmqw;Y zD9%(&6_ND-I?US3u4dvuNs|r|QOY#4#)KfuC}t{`7?HHJU}#CvFiGzz#T)L$C5VQB zOhXt8$0FfKJS@bcVL6Uu#7D+QBYY$jM*Mgr7UpBINHi)Y!(k~B?O;udDr_nYm&7P$ zq?85$e>Te@6{XoCBP$QVKqjOq^9EUm5~9u%VUDm(^cCn3O4TG0wjgd|bZ)rYJ|p_< zkwo_@#{C!OV#heED}>?Nu*HXMJwE%f&_MhLVnRjtl9OynGX?}OcE|4^73$c-zQ>xz zHYU%3*G^Z`<#2qEL$*@N?x^8C1kZQhJo3Q&FtJE5PJs8pKI6eY&GvW;@a;~(K^XB+ fFow=%BRS{+C|v_|AWn4d^SOTj^I4eD_zM64rGZZ` diff --git a/docs/public/search/fragment/zh-cn_d3991c8.pf_fragment b/docs/public/search/fragment/zh-cn_d3991c8.pf_fragment deleted file mode 100644 index 90aa31900eded97b9e0fa6209c7ffdebbf6f29e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3128 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 S?7H}`i~j|xY>B=LBme-4I~H~T diff --git a/docs/public/search/fragment/zh-cn_d7cd39f.pf_fragment b/docs/public/search/fragment/zh-cn_d7cd39f.pf_fragment deleted file mode 100644 index 71cc3901f35728e1b4c7ffe418cc27d413e8f70a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2610 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 Uc@M^7qyHTJKWxS7XY?8X0P#5p8~^|S diff --git a/docs/public/search/fragment/zh-cn_d96a376.pf_fragment b/docs/public/search/fragment/zh-cn_d96a376.pf_fragment deleted file mode 100644 index ea7f8d7bd5168bab64470c524d1092abe4f1fe22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2160 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 diff --git a/docs/public/search/fragment/zh-cn_daf53f5.pf_fragment b/docs/public/search/fragment/zh-cn_daf53f5.pf_fragment deleted file mode 100644 index 76cc7fb07d482dafecdfd0a70546ab1949fa3fed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1709 zcmV;e22%MSiwFP!00002|D9KRPa8)N|0>j#s#fJ;n_v+tt7;RqjiNMBh!mw&6&=0< zpK|tD=W|jNA(>bFz!-;sLveT|PzVH@qy+p5{w~e!-TAM4h3xG59>y4<{9)hj?9A-^ zW@lz+L(B*_%m>-~tdD){h)9BCz|q5szHpCE49XHe6p?u`*fkR2Sx(@CT(~D7vJrvn zBFmy6a8eH=`TV@h`Q(Vi^*A~mu*3yrtVvzl(q7E`ADyXhysxjk!~?aQ(bmU1@JOp> z^^>PS+TWxcKvj<`D7lUYXg_1hk%hU8QU*pf8&fx4!P@hD{pAu_TRha06EH|TL$e2X z&<*4p#L_FKp@enyNL`)M^Cx!yOH6E6-ty^t8%hW0&&3HsxDrE@|3;*2;*8P$l~K z0!r?j)?k?TF*?yfWz-jM?7nP!a#}8r5^%OCHH|WRXZaIoH$$8xi&9(H8azxZlYNOq zQ!#;~gxEf%RF0_CV8DFJ76mB1rY)?&sM!$hZAGb8O=oEfJ8JwG_~SWLDYr9UsV>`( z6UA(tP4Pjso`tTVqL#x^uI_KORc9(uUqrP^yraDz44W_pnBNjv_|#)5de|`eNqQjA z?FX-3r*5f`uuJH#reTkG7N)aB{lf=)z)_Fl!htrk?M_~=Etys*#kQ`~W4koq@NuZ| zjO~aZ*m<7?SgDRH#buJx1sudAxSpcS8=Ceui4F_kwOa($+!9J)8qP1E7N^nd zq7|vPMTWh>gpBNR_IJDbh!NleCy?>aO~5ANG#`~>QY+U8Q=I0UIs^3j$|_6?C95PE zCZoBhs5oinlzG;4swQ5OtKv0*Te8>^g~pDx6*V<((T!@-R6dNF@~N6=AaS=M>AesV zlmjGc1tjawPW0n~wRdK!zEPqjUtJ{Uvc6H@P1&$566Zq!)=y4Q`~bG>2LpQW&*L;X zuUxZ2lSWQMLp6&1rAJ9_q+)3Hxee9j?p{|5s=o730s8=aNgPbaO^2gQ+H`y4Ak6c! zIe;!H#Z7H{)?Vnf{j9dQWin{VBbAz;TQx#&ys`cF#J;@(iPi&iY=%7cmR?x2P`aGm zz0MYtp7T(u3k4b?IB8qcn_Ohs5~|MGBgM+~T#jg*L~kcr6V-B0w1OK7YT%cka@kS2 z8mOEXLS@e?Gnwq+e=0haS{`lA(q>6}wQ~jp_ZLBMp9R5nAqZA*(fh_sVxu;rYDtZ5 zlXEHBqtcEYm(D0z1o(&xy2o_s^NSB9^j6GFog z0grx!={=6D{w|U%Iss($y(Kh#Xm3`jQR>7FxjBhx#XL#^l&?q#zImzF()6kVLpM)Q zs!ZEn;H{MPeBM^G$u4U0txAjOnY+B(u=<}F*69dvGJ{7BnU@6)zdc%d z0(yXgVb9SV+oJuHad z7od+dmVXGmEFQ{iR~SF|h8U>}FGes~=Kdzqoen|t5i1=7&aqCre2{69$L@reBc)zI zoH2w$5|0U{41L_umtB**%iVVWJIs*4-5kC`AEMzl2Dti~75LqTJbe*bV&>c?+}}=^ zzD{VgSt?FZEk&NoZ55HujJ9RCGImM0Aqy}eo6cs2jz-7UY^TRH_Se{FmKDK==MMk? DBNj)~ diff --git a/docs/public/search/fragment/zh-cn_db72f55.pf_fragment b/docs/public/search/fragment/zh-cn_db72f55.pf_fragment deleted file mode 100644 index aa61fe47ca24f604bacada79cfca29a925a5321c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1380 zcmV-q1)KUGiwFP!00002|E(6=P8&z`SLpVom2gN%b15$sN}8&T(o{*Ms%oofygP;! z>s`xUlU5OuxtObop@5-a3g*(#0(RO!02}bXw4U|)slU)UXV$xGV^AORVDFi8=G-rH zBBY0gR9$(d$jS>T9@V5SsY5a3Sch!rW>gKvP1Vo?J#kf`nyS-Ae1~SpL<=aiKVZ_B z*&(${K&HCM#aL_G)$G*YiK$xdu(rCv0joGuT_0@YQ1vuppU(nne@ol|taVgklj|H{ z`!g;d64Eo3B3!gGV^(exXp8yU#uCyN4(!Px04C9y0ui`|CQrJ<@gUc{*tdHmVxjE=_h#i{=+u3_oTX# z=fJ%5f2Z^HuvW1A)lrsxza)2$22j2rIz_cC^isN3JZE z2F!v1Zr!MXf|y_sy#ar4@3bSgawdP>`sV*siXp~N+M4@%U~ zG#agEvl-tit?OglcHt0^AKrZmpU(`9=sf^Ej?gGE4dktsMk=MZZd7VxDN*4pm1Hx! zAhtN_>Ag=Qn(g`y3|B+iX*$zP!s6_PkTcFhE-kdiwBXN z4KBCr3P)D*sB$)UF%_Ql@@_4$=16!o<%J$vx-jdnVMjz(n1J?kAi#feLn3(7u(Jis z#T54OD?67G=?8L;;qb#pF`nz1K@ff){?t$#3Jgb75uPg)h16&aqid>R>WL1E7AFYL}odx@c#}(s`@p-+SDVi>F>z(FzG`(VYp(+4nd|XCu^?mI^`8%kYBcEc)9np{0x;OZQ z$F8rU9@Kn7fj_UTaN-t;m7TySZDCe#D6A_hu8V-uyyUBm;0CZmO1h@;}C$yhNab|ok z@xORPAtn_ARE{AQ4j%yGj&hgYl0V|$hmaw@!#y{m$}`!B!y0$<`i*ueq-rLO#-y$n z1MO0nnuJ4=shS#vJr66_>1UiB62C=sxz_-e?_Nk$qhYZVDr!HMuPPigl|YOyl|d2> zaAKUJP5L9;;Sj8quwP1D*9Y4D{5omir!sD*0{cJMNu4+0ll4d{+JOWB-$6?y%`|5dU_)2%O`B6-F2Ez=*O1{6(ic4 m&7R2S4X8ojolC7GI3qnV-SxBIn%e_E4EzV{SipTr3;+OJQ>(iG diff --git a/docs/public/search/fragment/zh-cn_e0ee7b1.pf_fragment b/docs/public/search/fragment/zh-cn_e0ee7b1.pf_fragment deleted file mode 100644 index 954c402a3f85131cd8579fc58ec45226c4e0753b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 620 zcmV-y0+an8iwFP!00002|BX{!ZxTTi{VTdpnt)Uq8^ep~OCR;Y2V;zBhMhq+EG$`O zs|g`NA}JrWLPTgH4FyYrRu(_7w2Q)jQD#{Fg>!ccZcR*lfP3bibN1dldpC)mkcg7N z+gQeF$COt9inQL0rb$nw#cm5u~p{mRPc5U(mT3xvGB z%iHgOYnY49fy2ES+NB(T;%oNulK?j$egEFp=B6G4$nswoH501Vdb}`NJ;TxQUYi{@ z9J0l#KUlTLn=K$N%rmPz8=r!(X|ckohp~4**wzU!jV_xuAhksZ4kJ0XeE0*Dfp86yA#-N3(t81}I&*yN z*wcMz_{@T7jwfu-a<2}wW6L(-3VvmCf2#1(XWlRVv&xsYw~BB=W4Ww77_BsJv6cO= zV!Tkdvw7&|?U3(xfim}4{RLd!pzAeWw8fiQR&j=yn!I-IyhoCxBpxO@9D+gd=X20n za9xBDV1zLQ*i(qt#E(Se$VT|qhJe@MWr)Rms_A%J)(qIq+v`3lqNtSUDJhiB_@p?Y zNFa$SR3&0}m)qm6j(2Y)p+*Tgs)6-US|TcmyZern9Z^QX0#fWxDVT(j?iZvX(v&;{ zjXA{Wvc9Zw51uGrs|Gqyfo zG5uF~+Dsms&vsy-<>vM6$Y(Ip&sL1{1tcBrG7h9_CwUUvh5@3- zV{T58&7Jaon9(?&$Z6|GY$@alZG%^S@F9$GF^=~2{A7*0D;tXJk=zN@wV=hq z)XpgN#v$yLexfrWcINs#nK&SG+434*RrLqqcL1JirDu3}xPy~KufR#Fx5F(Xxt=?$ zt!i_N#r%7u%&g%1EDZaswRS%Wf1lKV`6xL>z2;a-FTAnbs*fGoiBzAqvS7YCz{M_- zTI9&?v{w8h405uM>u@W+M#kxxJQ}gFtncS7%W5rV#zp7NF$`ESN00V8&6V9(#q6|Y zs4<_j&FI}(x~dPVg?GX`s^BzZIo3buI=g7D?EaqR-NhwwmtlM2@at&7n{wTHw@q1B@qeD7loHcoK6TBcwW_OE)dhWgxGpqVp&m5k6% zG9>YqWZA}YBt1p_vWW|%B3CgP0`KSKk&eKS$PX(4iMJ&bJm1M`K!M+Oc)EJ}uXpy~ zV(@T5|AU4}Up{|TbKf@AfeZ6qE6v3JRwh|x zSX-iTd4}WVqhfxo)f-k2{M<-0VK9@Y&?wioOQmwXKibUITR%LxW2T{|A1svtb{}7< zlX$4e9$!cm+}@^A?kZLKmLTwhocmt+`zMaV1l23jUq65;YM~(che8sJzQWI+1v>kK zLJ!zEu4ewZMke0bK@T}P#m~_c+ci?MX%c${gU@Li9PAhuv(#0XZu;&ynaNrABIKZv zPP41AqD6~kw8JS!EpL)`E$mV?N5jU^iN(W|S>jd7wCIWvo}lpRoi@8i({zTcjMF=8 z&n{RpOG|_q@F`xTwdGUhmHUEkPzd0-y9-`zbky06_t5~3Lw=r9sb6`;O+EPqUQ?ai z&0db{gW;DL4uGd{%kg*JN(d2ZZ-0NAYeAr|Ncm0d@XMYu+g*SJ!0G~3ob2{0sP~sgEL!AgjUyD&6Li6gQ_r|K}R$eB5T2m$^&wf*ZNo8MO&< zt}N^ih(NGp7{+D*`;vTDx1;_axj{*|J@gIxe?ZW|fUfJ87x4C!RMhwNHc(HYWsRCLwX)ESKBt8k zK4iTCv+UzLXvk-aP^Y8W4DbQk1E|Mvo*g7PXl_Nuo@kC%FbD_u4Zs`2Y>ln3%Ruay z7{snQTY4S+&k$I3idIo=EA@9e?~3jungMVE$b`X?dLeCIprXIK%Td1tHW<1P6Izn7S<&ZMgjnkh4I?z&jZamR+&0ZO9V-$v z+0bEI?KSJI9n?0#YAb?W{Z2YT5^5)Fi7U%jQEe;rb~|Rz+7fi5tQB=g7iSKehW;)` nTWObAuGJX#dgtBFO=~p@z5M zOX=bxk2oF`JB9#?vSW&(EyV%_$Qst!33O)RcNHS}^tKg=j)GQu%~cunbdE^n4;?s0E$HR5rzQBbQ-W zE0MPpOH0Kv%$&fDoNip@>(QZT%E($kf*xmm+pW$2Se)-{yy`sK1j1=7x{qffK)J71 zz0)OJ+TS7$Ty>6`cI7b;_WoiZ$2S%h+YMxNR&P2R-{Ic9dS~-K-&=X6T$$Zb1{*5l2# z>UL?Pdds zGD6@!xQ;em)V{_ayLyAGEaU#}hW+TCEc*NKIBuu6`4stTMYma&R4IAZtIJ+**o`N$ zuAg7eai+>+uhjzLr(%h_ZF$!zAIbM{DwRG1-R64xxQLs}r*5krK;x`0wvYFa;bGlw ztsun5`+?E!^+S8{HsV`+;GNy%h#%M$LapSipl9u+io3ooTkOm|bru%zwr6WS%1JGl zNik;EtAyzrJW6nQ2RqIzUc`Gc!;PHhVX@$yJtJrU4HVxgk!ArpcRc6Eup`ycr=$DurE{AHT zarMh^h-8@7p51|D=G59FvWkF?-PtZBN zFrkr8icmb7-O@D)!nGp`w0q4l@$xhAy7L2o?Vo|aTeLcSh$g{2A>EanHU z%8QzR2bHCjf^G?eQ@SP{p>-T!{U$(iUajX$O|aExbvl)dG3>@Xbxqp%1-sBU4I~#f4}NfDslz|rCwBpaf*7O zx-;8)7uBNR@07??yv(GLVd7qWah9NSkitVUQxP8*#hMhPWeDP`Q82q@Li2v*K_RB2 zA*Ml3^*g9MGK9?qm3~ZZqZD8u0Ty5pC-|lmUrN}@74#%5&kvd%YS5DPY;qS=*ozH&Q6(jSdNFfFEM z8E>2<+Sdg(VXAVU9>`78U8&O0490is(2bO$Pa1jtXCyf~7LlhXXYPsUJUbuc8oeI&A*iUaQ5z142j2E#8w{f2X$$49)O&vU)4yAUp675RiQbsTD@9})ji z^4uwt8vo5fEydqSrsvWHtfPOyzyD=?_a6rL@+sb-^Yp%brFr@YvZPoZhHQ!07~ zM*$BV7Kry8=9E`y>gIPpAafjEHh4llf(iw#HnFU zU+^16TD#)6h>VKgzZ2ywt*B$EquOMWDI?)m>x z#&!02`fv0~&!1z!$a~!iCX>EA4UeH%@Lv-C;5=cf%CQkjWE!$*LWD{hgGwHQ0wmp! ziuec=r-2d?OzMkVLcaU^Z#g~*a|P9n7<7vm=%R)X<&4@K^|n4cl3+(58o@?$o+t!; zmrcn(4!^~wQECiSG%+H1@ruRN7`}bEYQ!dXj%lN4nxp|bQJ^Fe`#2IPiC~2fl!O+J zj*h*>!ZB)}zL$hU_?dm?jDb0#+X@#!0VfG4-GPvF{2xA;{{QqZFniT#jS>I=)X~Ia diff --git a/docs/public/search/fragment/zh-cn_e760e5b.pf_fragment b/docs/public/search/fragment/zh-cn_e760e5b.pf_fragment deleted file mode 100644 index e2d797e4f7e297c489de3c73734a46e8583651b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 738 zcmV<80v-JyiwFP!00002|BX~%Pt#Bo|0>ugO@@PELb4a*i@xcjF~*Rty<-ifUAk6D z$dWppY-7L>91})l$l?^%shQd6`14(Kx9y(%3Z8QdWuPWJwCC4ze!qLpy|?Ei`DqfD z6md!v#CgupWG>2uMO8?J1y#{CDQ4)Bs*Fw>l1OApA*nF0%?P>-Mo36H5p+W%VJ^fW zohUlUoey=ly7<#v9Mpacws!$IcFEm&IReN%DtjlZ=xXly4D>pk9xdzupv_XCV}X@Y z-$tTSUT|t3F}7h1b}KCQuI&{v2=Z%m=_`QS=)T9wV}yd~R9nO1tAq+h`(3l&{eqwG zt%13Tfd{J0E7K#altXvR0`U8&h}>TJZyjAd?)Ub*8WL!yPP3iC)|)^=i&-cs3 zGBcRtJ|SX^7iP}S{V|_HqbDS0lknrAO%OR5H)KF}@$(5`(T6I&=}8XnV824M?csZ* ztwa3G_$QcWr(n^-X8r^~D$8;jmMp+(#OosiqJJ!;VMB zLR?&ub)u!X=zKcF%@Uml#OabQ6X@u(mhl_Puag5BuLubhHBaX`B9mFa{YB|HB#=aa zx;UDGxe?>FQ3x8~*2x@l0LiMr<7nZcW9iVaUg0n4VZz`5^1auP^XKBECV?Z+_~XK# U6_@nmk@U0lA69~w1_uQI067m-lG9{Ubm- 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 JjuqHv007LGHrW6G diff --git a/docs/public/search/fragment/zh-cn_ea4d20e.pf_fragment b/docs/public/search/fragment/zh-cn_ea4d20e.pf_fragment deleted file mode 100644 index e2530acd36b1b9c394eaca2cdcc6a7c28983b8aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 602 zcmV-g0;T;QiwFP!00002|BX{$ZxTTe|0=pqnowJ*M&-r$(nk}0F-A>u+%0nPxFg4c zCWPclY~^2DnzmRYrUgsDR1P&5=+VG;QFib67Vg~1d6<~^aJ#?x&HQ#|=Vp__a~zXZ zXcS?Bx#eKT{g@bEAPpvlH z-v~+;>FKC!kGAJDot({#V*nQhZI|b?)83jj_t@b(yW?~AL5CFvtXQU76<@xapAL7x zc{-*Q%cqA!r|sT%?cReqxrVV66~q^F`;m}>jl^YlPx9u%9>+>DmR2~r3+7|&dhcuQ zx#I$~;OvNQ_JcR1y>l@09a!r{w*r%vK5=l_ZPn+dGl4RvLn~M;Z!g4-EXOoC_-W+- zWMQSRtXuqNmT!$<7OsTC@`lqJ_%U8GU;bAzR;W3~CRFpj&vshiGAp$98oJ+W1%C4&=DtOH)xXa0&%+xQ|q!|Tmj;i zbQ|9kZ*X`O{t%z&mzplEi&`3n`p#+y#bkwGJ%u9KTnHsF5ja9bCJN?5@6DH2Gva?o4x<;8zgCk)nwLku?=XR&t@BUKJMf oScIr0kNpq1Fc&9vnG?RodrICrTF_q)=bq($0Z56MX7>XC0QNT@{{R30 diff --git a/docs/public/search/fragment/zh-cn_ebcf38a.pf_fragment b/docs/public/search/fragment/zh-cn_ebcf38a.pf_fragment deleted file mode 100644 index 9c12e64dd7d72be4b79945ded1d32d5a9fc1cb0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2197 zcmV;G2x|8qiwFP!00002|J505QybUyueh3iXlFnY!ok3F+#%R*=(NBTkv@`)=W6#N zty%4=`+#B(GZGH}`GWYq$!IOJ*?(SxFZjuaU?>M)f!yrn*>nqOd z1v2>VX0>qF1?RtY@&u{+{F)y;xlY6`6iUT4GRUv{amzEygknEmStsjpuFsJFiv?$n zQa##ncD{ze|6b3NrA>YBEZ&AeX?MS}L=iTglxMao-xP?XE@N257L|E_^C16{P~4v) z?;j+S{SRD24%zzkMient=` zUr{w5R&GD{V}fFh6d;P8BYO_d|4vK>R~A}HX}ehc>MGyJy$1xcbAzg~R@x?usb1T4 z<{r>B+&|@Cobwr7)2;35?yg@oE)#sLTA;2qJ5kQh64b4ietNzHckTNt<*S7{^v>ec z!3wcLLmkDjPuvLCBNj>upQT*{18SmZn%znjHFr{4XMOiFfJKYHjo0b?i`O!bb zhN*B(0~9Spy8sE*oD5W5LeyM2WM#f%(~*S?>P({wn;KG7T(fe@4Du4D1a>Z` zGZF)5b&CS|Zj5FWi87IpMjk0TDeI$}sv{Z8{&fr*YIqHGgp=Q(35yongp@44oIzTe z14eaY*vugb^Y=6?#7^-SQy)Tn_mMHoNAj44vWmpt(zYUF!96&YGxQOdH25T#Dvhnf zFq?gQ5tIw7j$T5B>ty3W2cKDV<{r_=UveHiqzUCt`Cke+JkthCDl)MA z=NiLUhK&P=prmFg*4WY4)7e;R7Aab{icB-aP0^#o4I$E`;V5Sv6-=Z3e9#q0NHxrJ zh61HQP}7!pJb*KbQ#9TE%j$DbL7U6SC`FS&i9}1N?v@w=O0J6{ISiQK_<)7nC5GKjxm$!2Fvkj#k4Sl z6FGY87%^NkoWKLx)_%Mr`W=$q;R&pcIe)6-AS~)6x_@t9^5TWQ!QOMni-19~(WjW@ zg*WkXD&YK@POF$9-PRg8w^#R`VCVj)jg7Ic5?crv$Rw`Vdlfx6gy@J%CSuotEpn#QT6tgvp_zdo$U>0?%yI{>vsbJ-?N2R&gydA zuWJ6iFFx~Gzomw&+e;8)dGMU9Op(EA?iZ9I;Im8$F{9azeUMVi!snHj>wiOw7)iBd zMN2d9BE^e`@Y}EwidGI&24&qx?3u)QZ^%$iKJrI ze1Vq8FqB9l+4R#UkWk}y0dZ#qh??cB=ooQe%$*QuDO*tkfShWl!G2+oACfk7JJ%y5 zKrBR&s^rjEls9RFEFYdAh3`CN2uYSpUAM184Jir-Azp9|vJi}+jXtELZ9^njcLG}U zNaaT>5%_DK6v1ehuw+F8?8dp!TBH&8v>f+o5@79`)PJ^~Ju;u=zC?<;u77!%24%x)Pe@|Zj zj_GW#PBqvg*A55Y+;gsPzVTg!K!xx|++gsUK^*qKqRajENp+uINNkf(t*q`j^E74` z$R!KJZ*^nCQDOv-p`3Xg%K0m>4RrWU^n-Nf-Wa%k(h9=rX5>3Ptt!{}T^G@=Z%Iru^VS<@z zkGH)SZ{x>q6mG4cS1VtX;DZY;uAqO_-V^~Rf&oSl7htk70<0AohL+rHLdl#$Ml(#>0460QOu&ewG!hGu&iHtf zpI$=&dI&baYG%+QkRw41O*#O$H0;8R-IfLDTM`ZBCs9hpeM7t)G#e<;@?MZZCL!qA ziNWsNe3}HIK1S^|j9LXF)*53G>@li=dW<^ajTo^eLoK5ksK=Pb5a2?h#mj{oS{MZ diff --git a/docs/public/search/fragment/zh-cn_ef22332.pf_fragment b/docs/public/search/fragment/zh-cn_ef22332.pf_fragment deleted file mode 100644 index 5562767e694cd977896f1df47c725439bd27c281..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 634 zcmV-=0)_n_iwFP!00002|BX}KZW2Kheihv-O+Z@Ii*jT1qL+H%g)zo7!_H7PE<0qI ztu_q_k`n&fiV?Ki({ySvVlf#i_8OyV@mH!iu6-Nt z)+G?V(rEwb1&KyKDsKNdP^|+m0$6c)!VCKn@K$N6Q;uh)VGjaDB`a!g!MEKW*LRff zW!EjF0pwrs&Ce2CQFOD$ceep-HF%~mq3_0R!&<&YDN{kYiX!hU-keawqtv+EhC6I# zN4CK*}jv4+OjLJUgN+h19&t>GZou|FQQq4WR^HD+B|h%(On0_6ymJiOG$a+( zJTOOXUU)CTMfK`Wb$75eec9-s&o_Erl_B<|=eD5$zf6F_!oW`7%aU%aKnfwVim4Vy5?#dqnS9^n3)}er%#;ox4TB4fbCH06wTa=Hzs;rP$ znAm{lugS#+C^#3qDosfJ5yoJ`j$tZ*EM+i>n{*Y>C6F#$p93-y0dpBXNcQz(YU3rH zI?&n+L7x^eOh)X47FtXCv>0JXB8?f$B+&Y*S-j5Eq#s#<S=>HI0}g<_!ooR^-73 zmXKk8!oVbqY`=6ml4j%y#7JaP9WjGiT4*lmo7G$BoSx_q<@ebCkfVthMuMGbyn*tj U>70H(n0%Q011v8k-4z4?01(_SyZ`_I diff --git a/docs/public/search/fragment/zh-cn_f16c903.pf_fragment b/docs/public/search/fragment/zh-cn_f16c903.pf_fragment deleted file mode 100644 index da77c615e130ed61ece192f0d6607039012595bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 640 zcmV-`0)PD|B68!Mm);s<3_0vM$i`)tEN9V% zkRYKce^!i$jiN2IB=v_y4ceAgco$_4%ayO-%x+n!iHR3`=G&S1W@cxOh?`tD6ryHADV>@s3(5u~oP`uk;j#F7j2Lo~ucui^3P`f9ai($L zik;r9UHeg}%{yP`Z@K`)s4?4pd;!qxwB?MSp{u{|WuR9ap4io00CvCO^W4DG#&m>< zqO~bHdl-9Z&AZ!f?8VTj77Ohd336R}fzB`P)X_8a2O z_jdIFr)utqa$`|J^!MyGy2b0EXkp>$Q6J76Q&C22EN{5ZuFLpr#;5P^AFqh#&CH9_TU&_ zi|&N&3VMz#yZR2mMR%Z&U1zf8&NRYr+aKHYk+)0CJsCNc^=DA8i>Q7U{mMVPeCaHA z0VWPsEI5NhKgVn4)BkEF$}Oj`fz!ZBw|W?|>UOJyKVi^YCZ3HY+6BAee#FEQ&0}vX zS(b`3!(IJVI0A^mE8#UTQN(7Nf;thQdeL5jCjw7AzIL5nO8~lze+1oYb+_-JHZ!mh!Fz zRkF}mp#V7zW)#Ze#0Wyr1UF}oFb9yT%Y-YsCPi2Bfn~l%&heRySwYYHA9S)Hrws)h aKF7O8-W@u}Uk&H)<$nWZ&s{+r1ONc^$vYAN diff --git a/docs/public/search/fragment/zh-cn_fc88f53.pf_fragment b/docs/public/search/fragment/zh-cn_fc88f53.pf_fragment deleted file mode 100644 index 126759c5fd301c49cd6cca58e6dfb2f453d165d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 580 zcmV-K0=xYmiwFP!00002|BX{$ZxTTe|0=pqnt)VH8^eolKKkN=F~&5) zLP)M?WBF6Wv|?4P1xwT@hZ?l>F!Z}9yLWsGXYPbjjY%Kw=C{9@-_GprOjfzWQkp?m zsY(kZZ|NjKV$@V~G0otXN%Jb#Oe01jL^Tc03=V$zrt7t;Kke$Y{cZZX13MW&17qo;%adPT+es@GB(*fPD;hLS_W(^_D&aCd0aOwXqBI-o1KyvYLZH5310yn)qx+!5=8#bb*8 zAudZ;qHL9s89Ji!0l?XyqE8+Fs2&{0#nbJNMa>N_3}Z*G?>GQ{*`?zwvhqdt%Ky#s zwY``Hlvr5F_WK9(m@wH~{#P=&(e&+gtmf4~*1PC(YNCnv=J&gc!arrnmMt2=A&wlm zeTdX`mgOvyWEL*sari|7v=qMtCV^O?6b&{Z+H>%+l$u&eF0TMQkBs%qx3 zBQGyUNJ`T=vvMR+C`L$zaRm_J8rK搜索结果 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + +搜索结果 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      - - \ No newline at end of file + + + +
      + +
      +
      +
      + +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/search/index/zh-cn_25a7695.pf_index b/docs/public/search/index/zh-cn_25a7695.pf_index deleted file mode 100644 index e6cd0593426fc89617200a8f973745eda03ac620..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36793 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 diff --git a/docs/public/search/index/zh-cn_5a2a4fe.pf_index b/docs/public/search/index/zh-cn_5a2a4fe.pf_index deleted file mode 100644 index 56d12eb8e598b8fdb9e389fc53509d2bfdf8b248..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2662 zcmV-s3YqmEiwFP!00002|8kc1E*NaBk6ln4k&*d!4M z$SNWYYO7*LD~O|V6Oh#;4NDX;ggO{>1f{kGjaIF-QfsGWrlm46)+Iout+v=I19t51 zobUO!|J=|0opbJe=l;%beJgKU@xA6d?^%B9^4piM?HU#|5A5Fi`QcaA1$L105zY%d zMY}hMXyykAp}#pOQdp{&_a8jcqNZ0NwoOg1LNpbm6@B3QtL8*jN zDKUr`Oq3DjTvbIHrNNpdw#H|y znPO;WZj51_;q``N4aXbaU^u~WqTwXN8x1EL)*DVSoN74DaE9SchBFNt3>yt^Hk@TR z+i;Fyli@tWTMQQ%E;L+ZxY%$BDQ0%$^sWOt16x#sg5TzP;kN7e25YT~aJ}N~kxmg~ z&mogV*-2DwL)B5#ynvctqIME$n^AiRweMn52c~?9sk_m%2=iy>mZG*x2R=kAby$O& z4zlYd=AMK%2HpS)dQsfXkOt>0!x*gHj6D(w{86$;f0BQehwcyd-)AV11v-dGP}FDZ z|CwRLKPGWxi!$IXNKQm@6*3Euxd)lAkS#{`He?@=ZDs3u3jQ~+YQ*O^tE`ZKJpr*Q z#Bzwej@ZYDk3xK$td1ARS!|TmYy37?X;@do3SjM!DeDgUI-FsFB$AvjYQ0TEwEAlQ zt8JU8)#xe263D+pt5xE%a$$G@y#4gw{Ey*xP*te~IzJ%f>a5^D!oUc7JY%B&3x+{| zm->x2&OF)C!nAfH69ufBjrv}cytTJA2yFEW%1>U>F~k1^_C|SjZ)<;Bhs>^%*<>4% zhmZ;()f#weAT|oInTYL0yh1NM^>p_}X4Ay`%%;&5K}4+_y%kq&&9!6RRQ2=iDB7;i zvmMqMot@<7J^2K+hq5}9*NM8-ePIYiA=-jPzEd-wG0HApc(|xDrJX6x9VO zH4xr~@JU1u$vFjuYQQdrJr8aT+@<F}fPLO=w(;#y&Kj;F&lPm&h-o{~4?@San9XgO8)~T4O5(J;*pr#{MBDUa1S3 z2Y&LzsXcwIJeTPyOn)-Ss@w2Fg!>sfU@wRLg=kmX#x1Zu)sGE4xnHbEJEohPg2<;q zLJ8qVMF=kr(n_%J6Txm!>z^5TtUJ&O5FZaQGBpqE+tI&n!v-ogZ!8ksRBZSJqGPzH z_#l-2(*(esS{G8JC{;*>MU8vij2|ND7q)e18)-*Vj^0$zs*AezVqML(!@f%|l*+(3 zN|vRf6eX%wl)9yPVE@bgn|irlL>KVD+^=Qg`7LJ-uI&sWUDdMkLD)+WsYSdK@e{~i zfM0@=hf%s3gQlbG8fQTqgH&tmfPs9zwF zaPnX&m9uA2vJ#~$P+oxYeX?Dg!!>&(p_Kkkd!GLxLcNG35o-!k%JUEV-|3RXf5z`; zHGY7J!g?ymYDfGU&OUj)eW%0$JunN7ts?KQMTK1z^27vK&%-(jcNDy8c;^w>hsZmK zjz#n?L|;er1H_Iao-T_BNJiAOHv6`xay&h7d$<#w|?o(5?nC=GUNr=rw>}kZq zh<{%uilM7X35pWf8kI_3^uL0&1lHRu9wm1AP3^)?IOE_fhI1UQ19vXmW_T;%{Sl#M z@=MNqIA7#?(^6zA=Q*2OiQ@Iw@yMB#VJ$(A7ewmj*rktf5KD}C&R zJ32_MS8>9Ea~RPUL?1_VzqSw9r{O#k6e`Db{wpmR+@UJq%X1IHjRt9z^6ekOJ|#y3 zcaC!1M%b_9mil+AV;RcGkT+aBm4&8XC+emX7CgYP2X~~2d)_$lmKVDE*S*p;QTKb& zx3C~ONR`FEWdwHT=!Q=UB-QIl|D>8de}nGx#K~2Q=bPx~wdi~f_X4A!d)|~p_tSG% zvcZ#%MYnd65Oi+`dxj~C>~{8d(oyLKPr5GM+DX5qdpk8=sW-Wd?VbM~8#`4i4L4C| zgkBIcN!59X?H%mP*x>mOvbBSCjLn^Tsp{^;Fm#H7^BFrk>7{gQ2Wup|I@tp&vZ<`Z zU~OhEr@lmWagytpAxE-+f%6-7ajUYP-9NjTg`S(B%MNFA7w`e>sRdQ;Jn4IPVzp}3+%bn?8!#d-2`Nr$=Sp#~Oa*z^jK>luJKN2Ut;zL~l|Euu- z#QbuA!QIO;dpJL)%{VLca>?l?ebuK`=zWLz)LANmlP_nh^H-k=n?H~TUh6t@;Mq;g z2l0Dpz~OihQNEV<2@;+i6?gfrM= z>39Uml}682mGK_?I-?@5C%BWp-v3yiDSn#%!s4f}ieiQX!)hV2lXb8s%m!oH0vPu3vQqWXjySQC_W U3dgF+E&p%we~d?`T~QJM0LN$~+yDRo diff --git a/docs/public/search/index/zh-cn_7d5e9fc.pf_index b/docs/public/search/index/zh-cn_7d5e9fc.pf_index deleted file mode 100644 index ee1775c320dfb81be28b59261d072ade6acd36d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40412 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 diff --git a/docs/public/search/index/zh-cn_95d5dd9.pf_index b/docs/public/search/index/zh-cn_95d5dd9.pf_index deleted file mode 100644 index 3efedabd5904e474d9eb8e77bfe545c2cb899595..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36134 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 K-j&#(qx&CJnz};( diff --git a/docs/public/search/index/zh-cn_bab6571.pf_index b/docs/public/search/index/zh-cn_bab6571.pf_index deleted file mode 100644 index fc5a4f67bd8ac429ed2401262c6f05564e669c00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38604 zcmV(=K-s?^iwFP!00002|6TnDcvZ#sJq~lv?Y*U(MG{CM4H7yb0Yd1Igb)Y`iD1Km zq96i_AR=%f^b&gLy?5!|3SvXCVQ++{*u{qW?maW-`}zIf=kYO@drz4&XJ(hR_g?$) z!4Ew+WawiL-1ETw4@_Qe+7-F~@`=s!Bii&_9a;jk#xSpg?Lj!>;ancEPUWvnupEW8 zO~l6kGTnk^hIw4XF@yj09xQ%X|3+vV67NK-p(xiP-s)UkFNHP>mN5vXMEnst(*9;7 z#DBG(kuY6HZv^cgSc)UD{AHj|L~qC+Osyk&viRVQ=($tqgUgKsJ)#$iFBe4gVsZS} z5xs&xST;q5&;wjPI%{6U!O!5|ftY&8Do5`3C^Vt>&4`ze^QFpBST00@;_snIEMMJP z0%u-CZ$h8-0kCw5=uPQoy;+3*qo2(=5j}^$I1|y^^4}9`R54~GucpE9XXl%Uc_^Zn zh!Ms_^uByxBQ>ts^CB8go8=8S?t}9sF{S#$F#ikdJ#r5132;{-rUHqNAuSe7N20}} z5w93sEu8O)f$I~*xb^!ZQT%6?92QJba$GP4XkgVmT?z1?g`SAG`VqZIO!(6@P9B1} z2JRd1U5@Cb{J33V&NpW9uo!LK%(+t|4*sIJOuiTU8k!q%k*#V*Oe^J~^Jmeb;n7>) zhV3OdXdZuq^ENzxz*iTsinTF}U$OZ*tPA)_9E;(a9C1wJt7Rg37P9-IX(sx&5N}A| z6bbX6aq`Cg335Tu1B>;bcTg{)P`!xm#*6q5v6s|~aGi&H1I<5wHYLLJXGFi3k4=l{ zgXLc_^i9*%Q-)^B#SyfqCAvo~(F}FqQgvYcpXqL5qSihKc9YugE>%d=Rcwxd&i*4A!O*J&S)HU~Gc>jMZj0R-2he#<#kB{P;9E_>x=l z&AGa$C1WjC*BLEegIB%~_9M(B zgFC}~6PA}@JptRBaOmhh9o^@m`!#g`9^L;$>3sAbk8*qNz36@x-9N}33)doA8s;kz z?|i!QM12-akHWMWrjyV}+!Vl)18XnX7SW+jA04UBLFm;0IX%er{@?nmG z#m&FxM51`W?Lf~s7M6OE*g5=q-i7HSXsw~OgVr7HPvAd?Kz|yD7J`;c+me0A|QTKT*( z%_Cr&D8-`b1}uL?!s6o@7U}e6YU`?J%#_k4@HNuIfe`Lnhk=0Qr(n9sp7lD2@j>(%31)lv?8 zu5NaRx|vztY=Ru%tTWRi^~4Hq7cr^7QfEcre;dJQ1dC{*N%VR7Tkk*c6~kWx|10#Z z_m7C5zqM?I zJI2DX67DQHwIx?j>Xov<)e`PIlq|5ehmE#=7GLmwm}ZG9UX2`^%4gk+P<=#M`Gwqz zz!HQeMg08HUI1rf9t6GIV=!+b`{Lw^+RTVHm*;Q?wDZtDhW0be55Up_mOgMN!QC0& zHSiroASq(yS7cda!JP zg_^}j496NcUV}3e&eg1^T%+N77XAhZnh{JwFdM;q1n)s` z5rY3AbP3_-5I&9Q62!CwHUis$W2hgChS!kN0V!2T8HSXvka7!+%8*uqw1<%PJJRJ9Z`eNyf=+G1`NMu>QLZ|CTb=5C9LcxxN- zERKgc3Fa=m)yP(`G=QZP)=jX!2wO|oK7}2ycZYow%Ol5AaC{HPpYSe+pC)n@{M+C^ z3jfCl_z-v+!TtzkBb0~ms}VoHd`nl@GGOZgTN$k@*rvjE6!u22{{zQ|aC`&TC}o>v zMnWWM^bPv+`WyPE`oA!>glP@5r(o^}^Ifof7YPdkqc5~uFvr5&4dw@7{sopsktjZO zseTZqMlihv%?+(Bw52e+U>*wdmoR@DiLFiH)7!$-1E%G$tRg!SmOo()!1f%RUEo{* z=lgKAgL@p@$KjgSpQdlrKZ)q^{KB5n zm*`|_7eO0JGL>J}G<^p#Qf z%)M8ip{%i=lr?sf#A$6DOU5#ot6(ODa~95J@O%re9o{&2YvA1sp9%ik2*e<;MeMcF zh+Bh%myz%e5g51TFF{Kp zsa-8>6j&$1HWM})eivoE^yCTH0TZo-_oW9BOTwI$bT|DD{SdT$&|ZUiAS_{)C3^8f z`stAV4$tL4m`1?#IzI=k8O!6aX2O~e|HBB*LvSU6>k#}6A)4BL{I)XmHhMdKK1}q+ z%lWpC(Cb=ZnAgo&bgrnYnV}R#@43?EaeSn7p80EKYiS?ILl@LWq|xJlTpB%YkNDx* zo;|Z8j@dj1ZP4vK^y-gZqtUAxy_TcbVU$j!EnnIv63dFcfQ?{YWTYeCfu#;s4erJAiQ+Jr|glVEs?Z?P4Z5h!A&ZGn4^;UY3K3t!wZ`DuhUq$pm(}a~& zs8`TY@e%!QT9SGSiK55yJ4CP zQ!TA!5^C4zUi`!j=q>an`)iuMon*&1;;HWD+pX3Q=;!Ha=JF)2tE=8TO={lY1O(?J zDH}&N0IS5K^+8L7v6Y9tA-i0S`6X?(POJM3S<+Z?_#5vy3h-C>>p#~W}BQa%#-XMeIi z?#_XKam34yaT?ka(5}O|A3nbrpFRkdW04?#>P+uiy4H)J6+%1B!#H?-#r!F9(V5#z z>%??ecv>*c<^zsiJ-L0Z9A_SRY;v69lURP5SGp*sD-nmV;C@4(5dx2*=T4M&A`#cK zD|+^cc=@)E!?zZ`D{|hfg|OC$fx+4g)-lq%>mG^aXEXnfz#KZ+JRjy2hUY)Z@chXU z$PiN^jLL6G#*-0oYFbT16CTR9upW+Bd6?5-DvLPSC?#XTLGEW6Trb1741u2!nu_>~ z=-wWsV^Dct#4B9YD%c+!I}mv{`_vi2_}L%?sOTxF^ArA?KTb8||W;Z!G}r zXe5fC;6Ioil%gi;bv0IV15z+NwDZaVirb3@ozSq8l#n!hi5B?BA>=~XMFNhmv5qs_Y{oxeNsKsZ_~QpKkB04TIQGGL zC!+2})HFnGK@@4^9Vop-tH_bVzR?Zo8})HuCWMm^PC+=8j9ff;At&S1yel(muB_TMFH(>0 zWF~?7ix5TIBc}hqe@me0AAVF_TufiiySIiX)G?pOw2jSfn;W(cuss6XBzT*_N1k;% z`2K)D7XCa$&p?ku5ieV8rWDq?mT9mYVfA788}1$OEQ03{yrbb=2=77ZlxXE_k(&w6 zOMs;VEO$qO;_G%Wr?6zRm|;nVrCTJ-cElg7ku4;+=D>0smbc~nm`!r)K)XvB7d@46 z(N#>?l_fKjAahNZ>&>YNm;~ludATf_DpzIGO=$K8n&+eW$4XY~{ekhocc?#;7MNJM z3GjA@_b`0@2@K$!E}U& zvg-2DaZB-P zEr8=3oRuUz1xy0-+pyfLuvs$snh7!YGR%bXcS0K@L=?U0l8D}eK17LUx;CkLx_Bm?yv~Vo zW5tQL7^BR&Gja(#-AdY9q73NF!Ao=9bh(zY#-qg_25#9Haa)zhO*DYyU*R9A@MBVf z_wv)kkwNnyA!q;lOcy+(c0y8f^*&r*D6L{0%R@dxut{y(h$h{-7A%|09bkWo7ohze zxNg8re*R>*Nxxi%KLr8u;h#i+G$-LzClR=az-0t(BbbR`cLWC`I0?ap2yRF49D;SU z5gu+}DK8fOGDBgwM&!J=$N7*!8p;Qbi5h%p&Iy1zz(A-J3NbKD_xUKMdo5&q5-5f6W8rr#6sF5~G2 zIYngNK_irAC(NI~{5eHM2pfib7sm-aJ>lsO&scbtz;hV>NeG1*>X}8C50KF@kGJ3e zXp2d^K>G*gWZ^H8{%j1#DMo@?(~&l^Nd$P)f#ML*K7{rSZ3>uEVeSzLO&5lRxh<>7 zJ#@6GcO=a2+4tmy!t^z?c`P{1`(Qo*%Pc8AJJNly*kSoen97SN4glMA*nT0OoKJyX$#JKqQ-u>W zL?_p*nJ_}X6hkDBtfl-ZPk&6w!%6yEFg>IM;xlv;)BWtB=hDAg>&Mx9Vg#<00GZve zJ`3Ay*k6EWD?Gd5^TPixg5>G`fUpnYI7FR9OcrAQL0l5zW+DCw#Q%bX=a5)~q%%ma zhve}{K8xfpko*@?!boX~l~J@U>%)kX*{4ybQDlK;|(SE@U0HXOEQPS{>*Mzkvmmz%&n9FK9y| zdPgzW1VR_E$4H>|Jpv`T!Jvd?HoL+0!EpXfm$OqKsS{t0L}+JWP)5NtR$&LdBYHRS zogbzOVXf+PZ@q_5HNvYbdP$0K$*VoxIWM+#Cb6mZWtcEictuYzebOmkq`3e#zr-eLX63O+2{1Dc{o zp{-THs6%A=@be@J^lZ0MJEZvf3B{(D@sIcF!=d>I(wHLFI7PH7=<8T^!nJ`BvRCNq zEfKwn|1OpqD-f->mikJ38Y;EX)p^sWNgX;4rY6FK*UMnC$f%aNH_V^HIZ*hKFr8z4 zY=2)N{Hvg4DZ}d=DZkUR7b)AOIn3MHjCCeSGefh%a!Ps!niJ*-X^~V86SYR8aY+3I zS*_4?3!46eW>e7YEi}7^W`Cf00h;$f^H0(I8?^lzx&4v*7V?0+mdGnX-Vo$XMqVxQ zb|UX%{y4-a+bJNPPim*N`z285fK#L9SSVkWGU)WTIVxWjZWx z8cRO_(-X#$Cp4#@40-6El7Q#s9M9jAych=OE_fQkGX|d9@b-jvFuWt-J<3wmR{`Ih zh#rLKHxNUS$AyU5iI}s9`2sP2AvTEEMu_c?*dd6WfY_C^cIOFOlR%x$0&aVf_pxa^ z`$80B(llt%^yUcxH7v}-v|<_M8LJZ-SOL>Im`Ie701ZN;soevO)^wRLyIL!A$o34G zL#Jm`Bv-;|`k%sXBhy`cy(O|xg%-=D^X2?eI!gB5aMHgXgKH0hS!nPO+P#GK@1sK@ z@^7QSgaNN%;6My~QmWVXv=cmXi*y!94Xo#2Uj+Mg4mkL?s&G(084h|~VhYEgRYUuP zOjBt+hGUe6ZW?N!2_&O(*^zR@pwTF#PDSb;NZ*N!ee$)EJuKcTAupx~CuY!ou8hT}cCDJ^9Lp30wht)C`m+dE9nwnJXGTYoi%rkXOqUGJ|hGt$*EQ(UmNiXZ-G zhNbL&5yf}}_G;Ld!M+*x=V1Q=9y2^u2x_YAA$tAS&3E;q18_0%ty{yqrXzO_@?Jr^&gk_ks(O)qrB_p^r#Y{z z;V@Hhc^0hC!&(PhJ=j99CBl{s+dvMCx!$EXB>Yq1Uk3km1X?3dhQMP8Jd40o1Xgp@ zG_W6mcWB)p@GF7|2sT472f>aM#YAu@g5wa3Ak+Y%`w?1%&<=#2L+Cg{KO*!i!cMj; z!ub?qi}-8Pct{yAO@J*Owx+OkrZ_chpTl+=cG`^@us0Qvyne6`f_*&f3t(RZJ8d$8 zalVB88tlKq5r!iLjx0D{B@Y?Sd^pP)5OO{M=L$I2z{}#fu0j6y*y})4rGWXclatle*8CrkRe*zPH4BA|1tD$WXk+bPA zAA$KTm_HEqG=3uMDW;rGF?e63U(9k@IaHB;DT&*>j>RK-G3a+n&j2`m$nIw5MY z0WV%vkljM*Uvd(RaD|guk)-n#hrryGgCMqCl6kN#C%_Tj@eJ$)#v!m1f#(tU5P@$H z_#S~jMXb3Ag1HD5AXtXreH`?R{txjsB*Y+LAQC$w@ku0|MA9orx`3n)klY?APax$- zT1IU3%E%1K-;W5d*lE~eYhXG>|E3^FA)5~r4WN+7yChf^2;bv-VHn>7^Aj-7hIttw ztz>$@d>-a6VE#@-@JNZrz;b}~4qN#YSI{gRJ0$zwG7FaVu*l~sU*(uHwy%>R6)l304=9^wkFt4Iu|Z-;J=aON-;`3%Mo9Be{_TwVO^b)w zt&mGvN0(WwovE9#-H7s>SJ>}^^uw9eS7ym5`tMh%N6|7kV7U>A z6~eR=><`2KIsE^qgsg8=Le@zGu3M$j1+oYDC+vlFsfllFlA9lFsfBk9u{<@`!vi-y!&2@Yk2w46c4~ZGr2sIOy8m z$up&LF!dJUm_2aNM%W~kgQ-0yby#BI>ZAf2sd8M6nxWIr3Y+nMj>hYiCbF#+(@h7y zCiSCkKwqY9^n%ZjPOGPof0OzvV94%G(5oE>;p62*nh0{6#v$Q{6>9jda@!_SICU_O zeEpTldzbKPA^-Y01jj@y^id}tC_CaH07WNLunzTaqUl6*u7^GYP&SPGn9d)n#ElK` z9i?EfM1A#i{`rK&kFI}X`G}_Q2vBga|4Q*!eKFN3Z zU4~b63feQGV4td-rq;%!ToM>QdqiSR+yrlxs$?EQG`A5Bb~m4&s6VSOTL|5#h*hvNJS+DKH+VLplF(2`!(?=5jt+05!&m6=9rC*) zzd!QFB7Y_FZ=mBEbi9Is_feFNqEZx9qUa!sZ7A-H;*U{$8NJ;odl^+gFLse+Df%no^^hR00LP|ysfUF?o(gG3TaGnn%TBx5Cd54(b;a%aNytBALYku)_twq_iydYnS< z>-2wMDurnl1tg(mGbUxR@&@S7dZ24WZ^5UMWNB#xP+QBuiQb0)K5C?Gl9!*mgzfWl z9M?$w0%;SGR)e&aNc$D(8q(8+rMVm#CCFHU%&81vnPH0Idr{dW91Xh7fNGNS#roYeF|>v%RSI*}bb&I_PkDN8`l)T?{<=wXm8x`hg@3zD zNU)!VuaU}Ne@kUa9aNymJ{3GVsDejpB17sXt*x82eWvu7mP(IlsZ7^4b%Zt%+KX`b z;MoVy3-C=;e$x&(dc$!STyZMXKNeaQw3Sjar`eQ~=09X4mJd}@-#n3reR8@?Oo@2{ z1AazjJyg0-c^4|Tqv{(B4AK%B@M0vELjmEA2=_*K0K(f5{teO15OWv(944R%vqYLd zOlt)caGOypa|D)syaET)?iSB5QY4zpgozM}4F07vZFgaJ(B2qiWGlBamVGmxk&{>F z?^b?n$&V^kxr3n~yDJ4boR2(RSF=m0qi#3lk(|sbwDuA@VDfYV4%7jTN2D!dt5fLo zhw^|uE0ksDdsihbj}Sjx+jH9ZAx1gD&(Y*fUa?dfHWb`&B*J-ImI?gFi3!#L5yw(~ z;S>i?hdT=H@8InN@15|a!q*AmlfZK*Sc}4TD2hez1XP^BfcsDthpN^XxPnAPMQy~z zqaQCN`H+a0r&(LU36;+Ia6JU~dAL7>XB|A`%(jjAXUHs&pa_*shPj9n>>OW-xGo{h zzeBqN+CwlC23IVT=FNkobEA!b_LO|jR*Lk{^9)7Ej?f-NJ%lL6zk-+PJ@a)23ZZfw z+PkE(X;;OGbYuOi+?rVVKJ^DV!A-;f=W{uMd6aT|9+q~dey>1%h0$gnjLbeNJD<%G zk)3ZksmwTQZ)vgUPw{J5Q8)d#+GO2oQF2h2Q2J#7J7h@MK`jiYS{N;q>GG&DT^^N} znXzwyI1jY9m}a2{2cV5USya&;mXEISJrM; z8z4F)2NIqu2jXfg-ytDB_t#CCKSK`MnTP04K_imUZ$sW}QG;En2D{7{>=iZGyVPJi z(_pJ7U)yt3Mo-~f3SV0l5pKzaE#d`OzCv(?lyVdw3`8re?2H@{Z9L(Rm|M`us%I>g zQDp1;@N_`%4B|bAZ-S;pVU#yjraSLGP4GI9(z8##u{`AVBFA3ZTuzJlKKP%>G%5sr`Hd{a4e=5Jv6 zzzDWeB;z%C+Q}8F7Nyq{==BtOy)R*J%k%7$Ta%Pqb4))eQk>|Fy7?Pb4u4V#lBODU zT@;aPB_A5rM`3+~2fwXaC3#t4@k@7^U=;H;ex!TR<3)ZXQmH=i#I*Sz;TvqbS~Eq> zm1Ug_&v%FnmEV(Rp*(OaU@eh(V~039o_@bRW!rrDBY`$Pm44-1qfo;dS)D~#l;dxt zZ$j@E(fcfV|A?wHa=BVg!ahc=pap7kJS;r}rw`5$oE;GDHqx%{mTLzlr}7NQ3AMz* z(p^RK*!A+0b*Oezu)or>7Q0$1WJrrauijv2kOfM$Wb+TZ>Z}X~Zk)J=(G4H7$jfCep9Z zatb#AYp!&7D-diW66najGyg{*s?0B<<{zD=a#fDQIUUXq&~qw!o>9S~HgITg^iw&FE_q??SHi>OissZ2 zdI=fgbC$rW`JyK$O_i&-%NBGwhpyw*{>-XC))-_rCcPmFCHlkh1e_UUZpz$}?i51c z3tU|}OT7kjE45%e{KNjs$9GPXQp)3oCmEhBcy_=;F_OLTO`=8XT}OM>GhNQEYZ_du z;o1s!Ds2aN8pHE99pj0kW87NAFWx|Nk@4D3WI>bJ{2py(Ui@4ZV%il6ieqBf_TjvX zLn7jNoB%dHW+xxBP+5qll!dre479Fhj>6+DMov;oIVB}KQ1S-)M9Ga!@k$pB^j>4x z{zsb})}bmmovs&>8z8QC;<&173p7%C@OioIyDvufJ@RdE%=$0&HcTC08X!)+GHI)^ zfq#%2c((YS0*5o@_l}M7l9mDqucSSTw2x8w0jeIC7miA%D76yV+LN@Elm(HfA6Ej0 z1R0fvo(nr+4 zW&z2!xf(g9Duir5!qW@>o2uw{4kxj>XUgHa`>K??*JU;8ld>9hkQnUM11DxG4c{29 zqY7>$IZ-Yr)BO(02dNa%-tzog^88O#_3u_gpUze8cbvHJwaJT>GV^>U&v#cT$WT+7 z{F*rM`rf79(&DfwvI&N+uTww|G3eLFOeYfzfg|sT^2pVcE$eIZP0% z8cFm4WuA>v=GkNFfZa86L7Dr&n$8fX?-S`{yaeada6P4jSh{rLw8LB+;`$tI`l8*Z zX#Y7n+=q_QC~%?MH6?jlzO&?KV%#JX1Qen&e~_>qIi<*X9;Nfp_o@W_Ohe&508eKG$I4W2?JmwacOaNh< zQMzoE(lH&SU~qp7Z!06cr%*|VzDB z9r-CPA3v_vUQq*87i(0mnntU-SfRlV*X@}yM_MS+BcKzU=|-s^rCUkh7tfRSthEyE zL2&=V(AmdofF!q@OcS1y?Ewm}vHkdsfkDktGA&QsXe>LkaxFDgYpJne>i?`v{h`X# zFHu8Uqcnhhk9-T}BKa`R{>q7XOW8@!^9e^TpIWp?>PlZBT0V~Uo6y05{ATF*HVPg_ zm)Fs&7KI%#-~;72y{sIk$CTq#u58cC9Lo$R8v*nujR1Ou3YaZYPc}?#&(9vO z$j%?7%)0Mju^46@$wx9zaydCYm9)aN>P7O!nrFiGBz#^KF$}`FT}|J8^6gr!ux)2yrxusj4?Y9uI@%>`JZVJVRbC~Kw2 zHdk;_x7?4vDUto38meiKdQX`##(t^c!ix?AUbHC1?Rx@^)*>wx>6esZ8zj95 zeKq6u<`3k&anONhOCM;|y<`}5FCE3RT|Ko&S;MZ5%9{CD{BUiufhwAQRME@x;sj3cl_YXXDCpBv`Vci%4w~(<|#Rq zt$Lw#YvotO|Bi%j41$`+m7Wj0fZ%SmVp+mXTYlxh5;e#HpQ(heOhaEiW9X|p`OxRC z?w}C1*f*9u)Nh8&Oytz4eRELlo0)3g5IhpPgv0`cd}$l-)s8(Hg+^%JuGM0O6cx1eb~ zG_8+9ABqkr=V>5p|0$>GerOJ*hC3)r=C)G9F{OxE$Labpb%?Ftr2SYj@QV2-7vOpX zu2FF9B{N*qW0NxPJ3~aqUeM1AH9Q8Uw_&=$gPmVD?}TB!7b&OX6=l7TP}X~eJaFFv zRrvUzK1bF)@(1a~t%3z`?1bkh<<9hkW3I|ACGYZQX?a=p!nzA7B-~y@j}eSe=?ckg z%mu{~4<``+wlq_;6Rc1lkd@icVmZ3@ypeP81ia&v@XMf3pnQ20t)WfBqy69V@sz2@ zdQ&~tQ|htq5(i#8xK;@X$1SCg7SgJozRpPEdqUY#KPX#jxw54u^6Bd?AKfumo^HLN z*1ket{7<6OS#+C82!7=ac@6K6Dkil;#iS-l3Kq_JjmiKDv_9^_#eq2q#0J(fp8mJZSc~j~vj**nh zC%M0F#x#YN_~H>5Cb9h#g*_jY09*mnr}XTt(C%6T?f%uU$yyqwSaW4>F%UCj+U29i zrz!c>7M5Lbd=2MU(#h`y%d@afkXlR&$lGYUVd({1K>70-&|ZRNwyYul2U;!H+==TR z-#JsB(S0&{eu&Zx9uHL2F1TokgpHeFTM=F7|Nk3?h=nBxaH z)8Om~Pgi)VCA$n)f)K>;F=zcclH>_THXDgtPPMz8@{!#w;64fW%WA{!qF@;Bn^TPG)9p!IX{MT zuh=zQrT@(lZBrafIh+MZ7?(ML*5X1XMr)NAU2LEnUn`X34ux{awRL4pjY5gLe~!ur zrHIxVBfQ^8*R>et++|~DOcQ5b-EzXfT7QD45dQBK);bsVI=E*kthJw7=x@k{K1MF| zJLM%#Or0jH-rFxj`xnsuZ*)jShaTu~3Himye+UH&QLqdJM^JDI1uvoCItqTE2jWvw zdm#03r0ziKdr18aY15JR7t&*p*%_HVkU4}4ATviHvlf{&XC2uSYNDSK@dpy21vN@r zJSNhjFT(VTs9^Ym8N;jvuy*76E$bt&J`U?rSSjw-SoXlW z6FsugqZ7(5N#2ZC_4oB_1WyolBFptx>pSSdw0~j#TbX9#81eQuk`5tER^<_XYQ!qu zXRMio_w;G<-D|thU)kiQbeXJpTTvH&sxoC?RKeku{3z4uX;j3?{V?1=!Tqa}DBr*} zjMGDeM42lhIgm1Cw0@i1A3piY+zm67C}^TYK@)}k(~fxsMW3MPTO~u>DkS-(d@_3w z_H+s$sPK(ep9`nR{Z=)EK0&C`THJ*~i&Leo*Wwqn^r2;awCsVFk11y@ZUf>@A?`fl zenWf$84D5*$|i!!h0 z5!|e#$reMJR2b5vzmg`;^5vFYTfa?F;CNp~s4bH)Mfs3$97%T~*@I;AM;<}j=cVxT z6tH(0ILus+0vaLP$bKj6_rlpqIlUKztI8}ArjJD-oWQE>WW^C?e}Z#jVt}L(Ds1ow zzp&^$BRg!d$`o&>6nsOK_@!ygoJA23S~9gV0<9P5J!D=8A4cCVU#6V-ACxoys@&O* zY2<6b`xQd3Bie`9gJ{zpIZIIR8OjP!wgP2aFklb{4#mJVvi_(@pD62(+R%28MiRx0 z9Pr$UU=M_6Dv}c$>_5U~Jok4pMN@kOmfb2G`wIu>L;%}28@?;>{fy97Db+Y+i-;;Sg#Tt`Y*HkkAh zk!cY8OAt&)utotBjm6AeS-Q_amFB30_@MaV`lNM6RQ5p?AZCDS#%lV_@Xy|sLWe>= zQs^YIal;2(S+!Q#*VZ>-o1!u%$jbOv85{|)yeqG^b+(eJg{M&QjMS$#CxWjUfsc@a z4Ze+#Wh4@kWnOEpyxG>-b#s=IQV>Ky6i*MqRiAlqxh&ulxc-JaAMUPjzXXpZV&$W> zWG)M^9_G4S>rL36f^8XWAHnto?DxRFF5=)U*Lw!@w)`ZF6uwPJTel? zMo}nLh_NDsHX(chQ4J7Ph^YGzH55@FAf}8i8LCgqYo4fmoJGp(cW6FQZ?Os*g(gl4 z)wY>Tnc*@+Qm-Ntrtj&?5?YbdMecb|{a(U0-(zoC(O>?7xX;lb6%A9B^H*s& zf4kJOAv4OqjZ48pKPW073e(LLZ|=(O%}QT%{}ercMz3^sTh#8*GMKW#GMYV@8U=Zn zIH8yXkKU56)0V<9beVP6mv5RYm3Yifpb!nnxOp8N8ldCvD9A$bHdPk#tC3XokC9aM zlL0I~VgQRnl=vpA#C{H`Pb2kBWNbmkCq`!A2qQDFzhaIvjph0-=KuT12<&9TG|4E3 z&0t(bxxT_1+H_^}tm1D}hESD=sx}xn3Ii3{LVtaNl*S!(l@1i@iSXTs8iuIJ z^d7546vnh)9uc}K=tD3? zR2X(ea06qWQ2Hav z>Z2?fWjQD-L0JXLE~4yD^#2v*L6m2sViziY!hnY{U>yd0he{7BJEC$iDp#R$C#q6V z)eLu(Vc=w1{#nbBbpzQ=(c>(7T%agx{U=aDQHm&(4Wymkwl|9YLGdr>og=}I`y}{L z67g~!w*IClch2DWoVf`sw8<_B;)o`&w&t}!zMtI%M-f~paHVo-tgkKeqWEXBvhweM z|1`5ZkkJ$_Mfd^1<3Z7`3;Mg9iD{{Wtrctp3Y5e4Fl^i4sDtxQxO|ZyTYCreL;5kn zvif=?#Py52^%wQG^sku#km;XBGXt%l@Yn#;6_^M`Xe4s`{}E+-XCqN8h!RNsEfP-H zZO(y>t!*u$UNAoj+dSCL!1gxmAvjvV(F2YuI7Y!S8_phZE{1c7C_+!=1W?})_#T5V z0^f4@)-#!_zdOew0(WuMWAG1z@jh`;2@-0<&+8iA$=! zibBpu#lfdUFg1=GM8y;`oEZ>SN#g*fG{Sdbx`(U-neWX+Ub$r8%oY@jrTYE)(-e!M z@6$NTS0@)LT@oFp>FtlTX>`1={jZ@LM2bv@xyBV@O&?Ck4Ldi3XZ+3o3pyUJ=SO53P}~R=@8XU`mEKmN%ESIs>22ebQ~EeU4G}#|ChX~>U_PVVi!vik)<}EN z#u?gvxN;2n-3VOfl)`1!)A(o3y)wUMx+?rEh2vRxpN6j|EijojM=H%3!@isIUuEQG z$cJszN?q3bwKABO8U>!a2+tGOy|Q}!Z_%Ssxa%{`@8W!^p%3^**S`|r+vXKAb1|7qDsMaO z0@61d*^SR4Sgx`g%`IVZC|`?AZfgTRZ?i<7#HAJ7Op`7nO7&DkNi82!s){JN`2bBo zvcJUbmyga?V=LHT_~!i#->y{oc2AQnK65Xjo@BL5l?m$6?Il}X_%L5~ET@o$_^*Yd z*)#WES+$E0FRuRUxB;DBMW-*(=?`>nht3b9^ONZO40`;6{`*i)|D8bj8z}z}<(EAQY>;!pW(?#@fygo-? zrf<@B8Cc><<$-1NL*u8=cs?3$MdRye5{o8{(4+}HGXF|~k_>s^t=OwAEJ;@pePhIM)7kf{sAT5pwxj< z=JM~qRHcT#2isP1HROAC{HjWK9+NSTvrIS{J6F^S2zi+f+r4a`+5cs{+0_j0a=0t$ zdj78k##O@w56S#@y^-Q#qE#zuxUvLpIFu{3%y#A&vgQhF?_QNSOo6Csm}b!0o-WdK z8|ioHqd7lZ%T+eK_e=PKh?#|$w-MVOv1?U$bg}-5%+nDpVdUYJ!?gl#D@EQ&wDQdD z)PE9e#Lv?P7BhZj+xoiI%c|A9ThzQ;R1~*_OGPYI%(-BThK)pH7nxzBy~Dz&Uq1-T!!pV`YhAUJ%Pqe`%fFF3LPhE0YY@K!$ql4Zq1b{D z!y>&UFc5(!5PePM87<>QLQ&XR$|sIJgqBaKJ#Kmc@oCCWTWchi{itx0T?#i@Audo? zy?K@ZPI&>DJ`>QX`Y2$-tdZ#X9(p;^tF20*ON6BYmMN-y19~53hnAUNiRxa?8w%~} z9MSKs7KYp%x!dYR^uhnnKg8WVc@auO zs4+sV5xS2V+PT{BdqjB=6@#b*L=_`y2%=^o>IhP{AT0xFt&!FrX#zrqXjZ+<{iL^s=?S>+hWjCS?nj_lECe!zW*|Tw-DU*#BJddk-y+lq zp`i%PKxhF%wFsR>=qrS+2)CrWhqJf}>|q36qr3b5g10G@|F&$?Z&X#bt7u@%cQ~Vpwy>Na?H*j)m5i-W@PtXmx z89*KLw5{Z7?_Q*_Wp>&3FcBp2OC(gAAbf1W`?#DdpP4D|yx?;(iOgOyC+EVvi>yR0 zZ8YByl|BSs3}9-6zMOM9M}ig&^m}r*DKZKYkOEThQBbQ5LK6|~M|1;8u%%xVp2J9G zK0Id>f_eok)?6?lG1k^XW^c8WX@Xq3;{Oo-ZxGNBup$tLKpFx^kr;&}LO6@W9-qxY zWr`AMWawSswiT?3i$%1lA~O=>6iR|sdGf`;D{3;g<| zwNoV+{-^}QAC=ag_ZfJ9fp3|#_LzKXK0~#1%KEx9)zhU^NqZf|X(&D^tvWloMGk{} zgKYSqLB2uetJPq2dfI>nni|kRH@@h`x>;wG6%lN}gbI@Pd^!AYE8g?2ustnluuti~ z@+sS7NK>RdbwYf(fj|7feNJRX)(l#QlMOU!&?b$xS-~?s#RSoK;53E*X94CMU($6w#fH z!2e>ym@J^B5RrYoM5%leWAM{q-Kqwk$YfJ>v$ogGS~*q0o{kUeD@oMP zKCO(RxM@a8Ya63E)Gh@w5gv1Y-JI&$I5`=Yt z!MzNgui&{QnN!Ip)Q-UXEv#o@{YzLIt>Bmd$3j`B%@mx=xI>qtwgdmO4xZt_2H+BK zgT1PRV`xx@29-!2g_Lt>7}=X>Z{sDDv7Ab~iS#MR$Vb~E)zF9hrEBW7E>y2|tlIl+q(!1FXBKJS z_wd``kB2{vL>>G+5VZ->;I26_M)A+@Vw(yijX;Q4a;6!NxS(HU5(T^Pu+!-s{!Xqm z|FA_rt-q&#uK%L{OW(d_#QT0DJcGT}X8aoa31X+^oX*CTJtjD8@Fc-IMsVL_~H~ufQq$KFw*Smzp`z!n3^!1x6 z1NC*q`=>2}xufvZ$)_iKq6=@mmIT4};F9Jdrjcw+A)Pt;I(-*G)20N0ZWJ=fvUZJG zfGnv@E8Z98D_mr$7uO~Vw@?tz(KH{1iEt1r!y-Ie_rUxyx45$mV3g2BKEPWDe}wR5 zMCBl=Bcl2u>JIjjeQ@0e*G9M#m?m)uEVmh!k9vt@Cv)Cp>a8hkwm@G_Cfz}Ho=NKW zWqa`gInNL3$MnAhb76%+ZM_28TG)=$KAuxuSF=;K4A2h3`HK=S^HmDWT$w#=?*jMJ za8HNt5(*oma5MTBpnM+6SEKwnlv6Hcyj{Af6ZHe3v=;CmoX_a!mNG^dUod6hX)` zYjPq7-kMGknEtye{?S~;KN970?m4EY&k368HjdPDHwinF5%)^&Hs*!UNm++gLzpC4 z<}|1J%IsY#(~q1NF*({R5{B=-3_Z7@_bQY=iN5LRzY+bP$AFLJgPFd8`Bylu!Tka} zqv8Jn0T04yMk?z#BlqDBm9X}{{t0GS$6sOE=58V_(GQ6FXiu#U9UFcl<6ytg4QBK z_5N!Vv_|o$k1M^K@+VSm8`+=R;N7X*n>+P;jO@=*T*^Y1KQ5C8gnlDHw~3URo=xy< zk=okahbL47D9x?8Yo$DCry5haszeBEHwx|Mt3o@jDV{s|^6@hfIhDQTRQ48MU){RU z=vdZSxlEl^TM)YfQNENnuqgU4bCxpmO?-jrSC4O9D$_R>Q(V_%e9-1g`pYMw&4<}c zmJ96hO!4PNF5tu3yF;NFPUA}_aA&`kaan41x9t z#rkf~kf^k$K| z@&-&{!oui!HIXQWI*#aHGyQKgN8U{oc==R;u7efmdY2;oYN$8m(qPkvBojU*aDw#7 zG!YeSr}v?+nn`-)7{!U-KyNAPVFn1|dHTJPV6EvDE!@m)A@P2%?=;#J*(z1C@GrE) z=NhHamL!!n{}&%V@A8@T#^(7%Zk~nwJ?F~R$N<%AveT0cQ5pXb;K&~KBed>tKB@${ zUAD=X&gzH29miskQVsJhE(K!_@{c)t!#5kf)}y2eN?W0{eQqx}UgQ|%H#7#vbF9&> zMZELm+8HT)k3ppjgU}6Sxs~N6oKL|$k+cmvJI#!G>PfK8fUQOrmk2pAs&KD0HCkK@>iV;tnY8hT>B6eu>s!3sovf z6X#b9Br@ISK3N52Er)eKtn?aZz)pd#m2moHTR1X1j#Cs+TOcU8HgOr72Wh}_NO+5OQRR%qP|5^2J>nqMbnWC@1 zCROlSK78rrV_TH7$xPA&$>&T_wU`93w-ujDn5mkM{eP5lFiy&qi@f6huj1h?Rjr`7 z>PW!KiieN*>~;TJThR)v3U5?J`6Vi9aZVg~Wz|fhs9>q84+yHdA+tDupzny%mjji) z9LNJ%uSyNPcf$Liiktiar%82~=>x}lsnKocNfD|ob%Kf4>Er`*RmZSn+_}p4ApB5# zT@~EW&_w~5L#ke|0p9d}CYad-j|+!+Cn!AfM}V+^LkQAGGHq6V&QJX42s-xn19Ae-uj1vSU?If53Op#TfVq!4d>laOIdlacTsEaX-!s60wTAzNXbRY zw`f!!O~;_wH)!??T2DsnU(jYUa_XVeN9cS4JqDovaP;3nd$`#?m6E#w{$pIGDE)}F zoQo^`h_ReVDD$exvd&$^Yi1|uL3sAVa~htjLRO!Lo(e2vAf!QaG}wTK&Czf#8qYwJ zM;ME^$Dlk-7RAGY^3-gjK!woTT z-(Z;eNtscv(c7B0!hqI$sBQg%+Scre%v&p~`gj5xHK98-N;V2?KjxvlLk}QIjYvou zGTX%1j0ghL*(=x0kl|dH8{Q=N5*czWJ0ooXGS1zr;8_XJK1G($Lo_ZNC~;tlhMSuZ zSfGTWO4#(fRl??XDq-^_aVL_dih`}Bj^bLC<8|4H`rb66zF#SQA9qegJ~|nZj|J*F zyNvoAMg3s^LH@9sLLlN%JiQS88T4GB91T)btn(#>Hw{;KQ-5VPuTo}nt(rH}Rd!MA z7uD{)8>Xk#?(M<^Vk$N4QE|(wHQc385WaP0?EDRd=6**1us$s7+ned_;AzDmfBa1( z4?yy0q-;f6J?_g%K=e~0@ekEgGlli>SVQu0A>#Qfyevt*Dr<`nzD6!PrJ zLzOsuNA~SSeqkqW>nqc?ZS=;h%tYiJ0RLViz3+a*$4rEIoI7w2Swu2Pffc+V!n)RHCSlOWe3|e0yc-dBfV~|v@mMh!=&(;&+chK-% zq*Wm8F{C@t{}E*%4~BUR7Z!zjtMKYA6<&Q@lvI&hF1r3MQ`_ROvXWP+r+ZT6WI7`V zSP)1+;7CMlR^j!&1M{P5zcRi2RalHhl9o7nr1=p=7~XcOA`DMzCH6@TqW%$qh#Dp* z9zq9RK00r{GLTQebWRz_;-g%;`SA5YAVsa+k0|U1_ZbP7QQ$J|D$*0t?JBx^(ETs; zJd3`4xC{M<3_RV(#KFC2JqrFuQ!8Gq^kys}a_ZR%P7yw(F5CAo+k`%!$=5ef2%3vw zCn`*6pOMtC6oCsyQbQ5UQ;ehr@}HJd404JfprS~`VB-@%;FTr&=E#*8^&gVjAZa;r z#vu1;^jBq8;ir{-*GSoSYn0v#saNucdL{1|-Ci%l-3y7SNX|5PTz)|GFDlTpK?Qn> z3^uz7uzp3ByBNvklN9nO$^dn$0@c5Tb+3N}>wU1!f%Ox3?C{?uQh6kh zb;$s-rl^PPsva_4mF2cklb@g%Hy_oP=$DlX&`p1gvu$$ut=cIt8;_{N6thNZ8>Chs z^%&B=M8;%;DI&&bHq%Og7)PLe&W-c*&U~qBFgZluB*_?IRuk%wAqnb^M$nA@qz z)E2^WNRe-jP~@8<6I zxs{lOu?x#bz*R^HSzl*eRBkk;BQ<)?hDbk%yLt5);Dvy8^jX5`*PUVY@XLS7%_ zJ%k>sQONqGTbe3XyDs=prBB{nQRGx8Teqon4)nK>yb8(Zk^D81|3OL&ouB=_;<0*8pGHly}nsjmK`>nr5xof zF>E=1PF>9cs*etz?NRowQ(^!Ae=ltovP5R>MYyTIMMj zWfj3F)5YP}_U=|zRQGhnd@@>XgA07X+`3J3)gW4j&^kuRql6cb@QPZn!PyATR~hlC zv~%bUEHLb~E6QFw!`E6+w|~hT^`x!UleSiMoT;Lx;TZ`{zrfhuAi1QiFN!7}dqp-w}R#OL7jHrKDuyh775Z#RDFl zQrlPpc%1G~E#W))QL8PW&vRIFaQBo>qS9`u&}Y#w&*I8eODb~&J5In^PdwrnwvRk7 z!`l?zR`BM*TL5oofx$cr?-Y1faxZ!B8F)XXr-Ju4c>jivf`fMW0`N72FB!hZ@U?)i z6?{*_HwC`M@U4VzGkhJS)2k!xaNpL%5oY664Ms3wF(dYYkj4!1WefAHbaqH+vM0 zlboacV#GE_kXC%n_^<|9#4l)_zK8jK(Kqp5!oeS)gWlFZVqcxKaX)B}vsC5cM~HSS z8QihfvW#mWtjVxW<}`9{v$KV(QXSplD2HPNXP^52MxY)7cXB_Us7yo+LewNg_ds+h zVmgbBKN$_)zYXO{DDTP3o?B6tg;90~BeWe5(+?e*k__w61o@*; zkc%>p%F1~||5oIVj1x}TWHM4k-bV=LhRmkJ&1Bp%l3kbi)J*D@O33?>r7cfnt*!Po}n!(wJt$XKYxE_V;32DV!ZLqpw zeMm+Dw3ggL(EOL6|E`1O`$)J(ATQ7JYwyDucHD)NVCKEht_tGQQQQ~P{1e@AP9$oU zY*r8!IL&jSn-PUaoNR5akHm6X_kA$^#3`fZJzV-h0F&=g_$I?w3*S5NC&2$J0>cql zL{sRyhxQOB7*(m_wvUB^AE(mS4~VS4GE%MV=p<8Yxwn3wK2Cp5gw{Lh)5vSd6Acwa z7svEDB5N!~&(r(rGxhg#$HwY+iC!WZs<9*Y|8$X{Aay6SNIm91Gj(EHmX2!6KJ{&w zl3?nKT<4!TYJ&An0T=8N$if(orIG>#2 z_u4dOc-6k-tTr>nGX`<)0T)(GhUIryJ+P+1+Ja+bDEfFm%k^vaZcH|4e+rJl zJR6_TcubdIPNZc+ntCeDIrK!P@arr~qJI%BcPn9fOr-;q>f=SnmaZygUqDR1i(2R& zQ@GbJY5Jb?#G7ra_tWns3vm69C{1ZA}ShDC5rm+ zaebQtrCO@ylGF9w1_K_srgw-ApcMRGqmzAfgCFH%@019dqNla5k5@R-E2tAD$LU$EIcw8v7EAx!FRzQWu-LJ?W z78_YVr6Z)>;69<^*DZ{+`j^F#SLf|hK#J#Acz=fPe)L#{s)&Rp-6J?h$Md;DQ6S#* z4CTfDC)(*grfjuqhdtk_tm>Pscsa%?uVTKu_MsyR^a*Z2{c2UUkf2zn&Z(+}&g%78S{tlZI}KJV zd70~zCL7uEJyg_dp%L|3X+*u|vq`p8t%8^k@?TO`^E=9FepB5heUyQCKdun(2ly^a z>n5F4*}Mg#Q1Ar04p)#+W)d<}kR7i$qOA>?5!qQ^?VN8Fby<;!qSIwgFEhG)%~gvj zQ~YpkhGAoJokAZK)PE74^D3x64Cc`)s9y&29sE4TrF=US5myiXw$|uI@rEKh>Z#m9 z?iVn>wrY6`j*y4eM@sA=R5>&D$=WwaSNICkQq`{qUI^E{0hszum;o`c}ms`QCK~bdjl7X zhN=1wN}MgwH;L!IK7D?*j57{+6$9SDfD0J#DJn0a>M0C#VW3YY)r6xE)D%l`T!iHTxHd6+ndr2ty$y2^j zBqYAgWnjJjLRl+RCd-U`ep0fVd&61Dp|3y`LXXOPtevotDIG0^+5*XKV9JGgzTziN zZ*&@MFegykKN6tylQKy$Z8#tGwh=pD-lINhLuBhyh=la!O@EXFiI`#;BBCTM!a4L6ds$J`cP^llre|7qkQR*C*z;uUb5 zNhGzJKZ2Pc&N%p2AlZ!MwMc#)$(ND*A5sWwY=M+sNEw8bSx8xpl!HjQh}1ArGm+XF zsU=7)N9um09u*nYA0zDxt|Q5K1R3LyG0Bh-eaQ(oWCUX@ift)gwBn8y`RWBM9ht5w zU34BT77pBAk!efL{v~Liaa|P`9z6xivt-q?$$XYn&{g3l*+oo}LI&MfIDW^}SDIm) z_mSu&%nu;?O~edE%o0vgjyaE*uMzV%Vnc{cm5l)r`z&HtDfsdk;mULqWKLs6m#!Vc zp(J<3ENbksg=&>iiG#f6_g+4+xi(9#>dpFTZXNi7T-5*fFO9>+Qn#7@7JW!Y!oD01 zFC5vT@_U(J|NIcHpW&{6X8}Av!aEw?16)7M)QDe2tP}&L=VtzbHrxc;d>$4bqk}e| zXe~I83Agxia|FOt$^|a&j&M&Q6?}$Ewn5{rBU<&XC@Co9cK-^fwtwVe+;wuq<0S#i&;3G8r3rXFO z976K%q6vH|8l6V!r`#JnZ4T12kY0TeCGLJ1UM!vKD3}N~`3fe&o&FHG+;*{r>Nr>KN}3V^q??LuHBZr$?1IUs ziYtzau6V*N*rVKn=_G8cg@oOszfVd{Pv?L2(yR4dOX>p zw~-i&q`Qzb21&D!HXmurk!eBZ7s$MY#zW9}EE*p}le^JmBr&{UY$ zzg4vQ>P+KOtWPb+hucGV27}$;o7PJu( z*&zrnhm5c3A|r{Ta}7n`?)8EJu}I`1Co#_f_b~l|ENW)W(~Fl09A}=shS^3r8O=zb zwFX70L@8WL;I0Sv18|>$`vdq|BYYO&uMnM%=uU_xt=DIoK*BnRGLn@-Z*$^cjNXhZ zG$c<#Z+(mYodBv%s2rJ8y(?GORq*?sq^}i?D(@k5gyl2^KWzhLXxPs!Vh5`atc)a~ z4ll_m%zEeR2H%}B9nbkCoPS7iwmbEIRHo^l22b%egQs|lkq7h+Tkd?l|8*|%j#Zho zQQ}@?uFg}<>aNp>;P?~nO-Q+k7Js7eSPbZZ0Z*ec36&~)fV?8h`<#m4lD%eh*=k1L zt3op*oOURueu%7wq_xLIp){kzxx`*>*iT>r*Oq8b)lTsQ^DnLwbwZhnzD-sftU}y2 zl~I$VvT^$wIUz&fA0?k+{T$^uRwgLY@}Y*WbkgvZR>?!RPF99-W)@oZMXUQ1T0$Zv z?mr|16`{shkpgHUhIb*n8{jq1Tz6|&(;d_I#R5{be zGLHK(b4v&-+dJ=))nsoDXH({nC5O{F9L_^H7yaCJGu>1(C8P+^Fs@VdGw;?_X4PQlOYp!`Zg@!N7ew^l>;sTF~y?ju#LbmhB za%IW6IiA5>b0?VllP2LGyb8Me8KyrKeqkk(`X$aY4}C*NI7b_P&u%rx6y&d%CVYxJ zVF~d4xog?{eV32UQof1h8v@j~Im~mlUjUNkw<3q1R@Vv{d2U zi_CrGd{*7hGTlg=AFl-d>-r74TFq3$@7kyQuG`A!|Bc2~GxhS(9n&J3cm++k!JXxE z^2}iE!wqz-!^FM^uYK3(RYkA4wqm=gN;H1L6)ztgxSZ zGV`6TZk=1B9?Ya3%p|=!69N9CpdD5g=`pgT8Nw6@vgJH$^RyzAwnW7ziebJRw@j38 z2+}n=t)SXx5lUCdR8K2#XP^O2Pd8Fq_Zr=N|1`XU-9|Uxmk7}nL#6*;S$9OuojpeJ zi^^7q`V`}+D@WH|pS;L`#g{8syiUR5gN!?WFDGFC3FCUJ6`rIk+`uIcxHf%<>U?JH z59?UA!8~(C!ZVj8MlpwhBU5z_y@pbi+D$P7ITg7IcCN3)U2`!?5@X^uuTa>QxGDD=1G@>H0wgMyljAGTxSIUR&N z)rSc_l{&7Kwfq3@BrewV`w%XX1sq(daUZM{g#BG*lhAR!WXU^W6C-80RcBFIdz`Oh zd0CYrABFZeY^UJ72Iy?cSn#I9RHb zAZ;OY&+NNkKLOWNbX$dP8&PruB|o9B8+}Kj?*Ro-r^7~pf&WBpl(1GaIKf?{FJ2=V z?=r)c4^JgLr{MWdZXCTEGj3%{`aJURqLs+xXm}6RnW?qtbXYUz%Cf_fUrGN`nuT9V z)vo!uw8>H=I3ISvmJ9C{_zU5GT_pVqlJXvmWjP5zuY>y`CFrgRhU65!JQgdkID?=h_t{RHFgArJHpEu*&`7)I*7(whH#6Adp6bQnEAN3C)ek9^2 zAYm>VI?!-AidLa$7b=&@k@^2o0qLzOAe|&fXuAg69|(VAP(9};4@iqq>ygx;W>T%` z>&Up&bA_IDvEibT3*CfK4N^L`MA))m??3?tID1JJL%o*iD%wE%NLAoIqfg`Qp`B7D z`!W3$P8DSi2EwWb@IA^n5?{riI+4VsX_;F$*Ju;eXo8v+UzT$3zBGz0Z^=VZcE~&nIgeGrQ`|3|g0d1Qo z*yPSLcdNY*S-5cYViI^x9DEtR37oDNXl7JpMJu@NdllQb8z!5IZM5Omma-wn44C$a zaR}p%v~aU1CHrf zZ!s$Gq`2ofK@WTs-Z_Ywh?rLp+Z?frku(UIW03hQGG`%k2Qtqg^P&;@Xb1BlBlOXL zyH81mPU{maR;@F*VI9}izpVhNwfa{50D0m+ay4<0QU4+~zKc_(ckQEC8lb55qS-Ye2;xpuT>-qjPkRYA4?d6k`Ur`$lHSyCDVt%_80 zuj&JvDNp>rE`8Q7RYl`7yo491{g=|uD0Z%=ngG3~QoEVmxn@mWO^uO)d`t>gCT)^s zZQLrUW=q|S<*LIgRv~2oS_jbSZ&VebYA5daPl}|dr5rqnDVAhdg2cIq>lq~1>|*^l zQ3)eM)y42yk#Gi$cNk>YFB)XnTMUG2o1rt_RVfV@5S*!0MT#0fm(bK0)DnFX<=|bz z3_klO6`%cTeu%wwv$oZyDuvKpDTMA3tya$!Jv6laaDEDB9h^7WJaakW^25~zu6A(! z0JjyM{ks7H7EFN=Y-Q4GTBAj4PP8*Xongg)C{4i2-idS zM_R&y8b^;XaeaSiT}i$2H~q*9J;KE#T8feP=2X;f@yz7aLiW@k%p*qx705XthpR1r zUP+(NvHz3Gv!Io8ho5FUd&g@T?;=I(xpxe%jM5ZxHjKXaAXEkrj| zw&8l^k`)RbziRei`-!s0QdKV`aQP?L9(kHAX48*KGK@0%Sr1UeA>^m)*T_WS&!5rf z4}?r0+~GBZdURw=PAP(N>hjMO6_CRQ!m(Hr0~&-KA}?hmcpB(Quqk1{Zggy zrE%bf&e&#jk9r=ZS<1fmz&+Lw{tp|7*<=H&+s=-W^A81M{v;1LKF2tq zr#fJfI$#bTaI&s?vvI&Ib-)Me0IqsjaO(02?!>@MDlyrJY0F7yS@$68O=O3W{VcM- zMvKvCdkWq9q39Hf-bB$?DEbM-!%#dC#b2SfL+)_96L!F!4SPOkxwA1x!FP)HG?^)# zQ7xIQXHm2Hf(ZBKkmD@_c%mLU2ByX^wG-viuZnWcerj`nr{5CxkJ#PUbV9y^ip(I! zhnQy&^CA=V#4bQwGLnX&X+5=Dod2pi`HrfusPhsNY`f;b^*c97_kD-x9K(QEAXhrf z-PKZWrmyDEBu5p=-xl*kkfQ2|mtBU!4X7Bbs06+jjbEF{euFYu>}|!{m92QY64a84 zH5o0nUpHE6CyVz$7P4A?q34tllC6x850w$pkCxKHIkoL2#P^uU5u7hEKI0!6OFa_H zBlW%dVG44JCRSgIID3_78lrJwgQYvOv{^Qah71%leiZgG95!~;hodDNW8j+17M}YZ zcm~4rI6Sl9Sq{%Oc+SG}7JTy%=!#%i(los$)UjSb;L}Y(`y1jU6P9;xadnX`0rqm( zhYR*44;*Q5w1K0W0Ae0t`a|~@5!-xmfgnO-p?8*tkp?^qA3hp6G3E35A~0zzxf2N2jSWRcP!j# zaQBA$0Nk%}6~AX5Jd2rx%CifeeM~Cg)i^KI+f~#b&470+yw7pRZHgm#zlHZl_{##M+Yl9v_^g<^2ej&o9HwUoqM9&Q|R_9 zx=%olV)Up&k81R^pl1+0$D`*Q^zxzCEfm^OI01##DBOv{Ln!Kv;wTiiK=A`89*g4r z=sg9!PorcsO7@{NAAOsk?A9O5ak_EekaNwLitlDe;wr? zpyCl!Jb{V>sQ3&6xq8pl44uD0&tlSfj1$bT+ zZ4)cueN~j6t%2W-U_3XY2s43@V@Jean<8397SrCmq~aO}ME%FB6k8Vi^c&I0ut@Nk zZDW&3djr~s(7qP6cmYv8_o!Iy*97g*C|I*$?Zp8=PE)uF*W;03Z9|a_+(NI^PwVe9 zS>ke$9=C^!a(P#shvpUi;QDZ$gtiFUb0P!hRY9V5i*auD8mo+&^D0=wur`f^szvum ztrJ(hTW&+1kvPq!E(Auln(agjt=H?FkcdQTo(~6Ixmd=-vy>! z%PrpQ6JXy5`^&KZ!_|3C7o2C{ybbqEc%Fhc2yYC$aqu>Vw++0{!@msv&jkbXHUthJ z@DhTt2qq(VSdhd0hfpRDa~U%sw}k&PQ#PzrEey(-E?cC?v}91G;cQXv`?5&im?!3a zyJ&~vpeG|oFnO9t3|$RVPx@kGL{E`iZ0UMieKvO>$e1f~%U6ktr(dAe6WMjUn2Xq) z0&{DwrvHV&xEZo(V7f^5Z6PMASoHp)=-W2ZjZKybQjuc)PW>X6Yt<8Q)Ou(`MWqqN z8Q&DNCznK$6*SNnxwmToFL(i{Sc3q|;^!GV3-J_&%V2q*I_digY)> z)+Hh}@M-o6I4|%oUbL(QPAV&-KPkc~f>DZ`_%^Z+T5|<8Oi@t7L^jv+*?s5TV0#13 z>2SUzVBx2^B@FV!^5bz<;1c8n$gJIEHAOxh#U zlfDvx;|$Ifv)N($m7y~B2yFZ5ifqPrl3WU^E0vi`Q1vc+<#-Vz5&Z_puIVY9td`Ik zvZekhfo%~2+TvBdMPsIuEE0%NeJ0e?Y|uuFtf1whcg+`~Pg5>ay0VkR!k|v%8q5^l z%3fX?=HEo+BLP!0;rx%?K==1>{{w#|`~%@15C1-d4kIdr=$?o^hv4ss)_uwOT zTt2>1rN#J)5lZ4bdJzU8>nX4bflI12TkyJaQQDvshLmN7DkC0w3+f% z^LWd5Dpc`;OuaIBV^VKD>wpHvZ}@Q zoJd=uzYzevU)=P{;==~a`J(}Iu2rsdf5Vj?st!FJp?4)@R9l!v3et3#GlWP?8kl{g*iM>h8sg?4k4@^esZ)2P7EkDP-TScV#y@{376DB43PD>~U^(?jer`vSiCl=;;91tKgUp$05l=@}Hs# zZNf>}gw=P>N9RfC-W^3_<+Es8I3Lc^Nrp1D@!Ubh60ORD7I1~ETz)r=2A^-r)uq3n z2HyBPG-+n=u-zaaL6%fPi{_#>8u*6mwOsNl3grKTtvhUe;dlI(bAY!eKXr`F9`YQ!e)lDGYnSL*4RGDE;b@g#CiPtfHm+NjMy zUW&r3xMS3pN=P;pXoEgbEIoZXoe(5cDnSqcO9p_J^qod_Nuo3kY3!%3ZaQwvU^^qt z@47LAtBo|j12m#l+iG(qfN_Eg`5Q?5LVeb1TXG{Ou3RY=Yz&VvB_P)eHeh{8k#6(% z!Yo+}%b&1%MOnroSm}LJoHhX4uzfDLS?+*+D(q`ve+!PLa14Rtc{l@b-V%Kq z^WeG;cQoA1;9eH7%@vdKp6Jv)99mDYrM8P?^+Lh5Pma+0goYgAHQt1?4zypmC9Js` z=A$rwN9z`r=V42QtqisZnVPUiG0(9hB+!?OaC+dZhN~l7x{#Bxz%FoJB9KMB+uHc##s1lr*H|Amt%6 zYKle!(C9%ldJ2sepwT6ytwP!xNV|yiFtW;!^)RwFAnQ3~y@;$|ke!R{JCHpT+1rqP z4B6+=R72BvH0_S26=)ei%QCcl1T9}gYX@34Lrx)b9z)J+XuAz3NNE53q=H+-h-m2QJjS0-_g50N)k|#juHYi3Q$^$(m&95F#29WzlTwF0c8{b zvZLZ5R7^z0YE-<9imx!hi~)%l@FWK8z<_sAsiX1_R7InzKB{J-YCZ;*lPi(E2-z#q z{T%7?^kVuSS&d|6k;%Fb8XguE6G;=I8%;+8BC z(;flK4l>Y02Z>lwywOG!Z+rynx7=UFUJGX|Tnz{n(M1^GIvH9bL?T zYS!nt@`eeKibP+m8kWLbdPEcA6u@E|!Ox=i*B@lOi7;*R0$6GzVcx9cg^AZyd zdN6f>){Ef|%af!DV7Vw+mImvSncrffXa-y$sF8k#c_%B&Xxa2NhW`{264FA}w#*ei z(JT?|Tny_$QPSaoEfcopu#JUnHEcU!JIf%eqbqA|=TJCDGt-cJHr$ut=_cMvOLz|o zSGqBR{SiEX;OhupM~GmmY=lY$ed!Ab-9oq_!Ve&-8=?k_c3JNu>J!Eu!eoEU7a-OM zeUiRGKO;b_LeaC}h(Kx%3albSbRLSECD4sh{a%Xht!29UOi{hEi~`l77bY2N50EAJ zQABSb>DU@7{sNida9i}}6d4ZhGNq%S4s6D+@D7-+aBOh|8GY5F?3U1?UNF6;@{b3~ z&ZnYn_9^ZF6D^9S@?e@wVQ-iY3ZHlk%)Lcph*7YR;C>ml#jxEp;x7bWH04p=PsZ?4 z{R7UsXf{`X7uoug6iC{}_k2W9H@+XSuOs$X=oZAC zLA(j^;3TPpBS`oh4gWx50usj|sS-(RkaP%1uOhhxQXWIf4K#WHsbi2)jEoJ)w4yQD zZ+U3^D6)DZYcR4#(Cdlqg4iOGM8RwXzmkn?C=7Q@AX^&O-Dw)nSJx%q_`VJv6?Z52!F60I5 z5Subjl=jXPh~-5xp?PoJFA9kT(_I|Job~c)!U8JN%W2atkr0dipnguU#Ftee}Rw?XoA%wXB!D)P4=|8)%blGaFaQ&qv0WmIrYG%C36F{nK!2o*7yo1&yGMS2C& zxu=EuGu2J%4FybSi$xIbRY*R z*C(u8pL>++b3z<`W$Ce55`7B05Uxf_QxqSSUP`bUp?cCwaWvyViev}a4s#<#17tDM zMZHG4s7Xz!=QEXtwN)i6+{t%4er5UIB{E(3VVN%cu&hWokA~}AxPL*QDFR~(04>7PU_HG+G9)!h@Mc~fEh_l zMWU$-DgD)w!ofBI_W2ZflnKY?-lAudqZWeeJ$fqQ>mlBS_@-$1FEU#ovk2Ym(4!to zmvD!*8f1Ne>{MjeAp2J|_0YV>r=lTuq2xeJ8?oj@2bX+(U&JR`xP+&W1E*Ijo^?W| zDE9f4kzY(E#x}L)D~&aOLxJ>N6-Zw|lggcYt$VpT%K90cyAk>lp+C8cSpj0^A?7{A z79e&bVoxxd^d6j~p^F99wga}8ILhaQtBUCjIm|X3?rPE6>t33(z4~w563+CpK(ota zP^DPOnO+oX83RiX0;Ps?o8$ZIR!=uF_~xh#KGoVe_XqXvlh>&2#C@Gg#F~qx6W7i& zz|iRm4DHC@u8qh7Bu`T=q3Z`vW7%-Q&b{j_Hz~eGD`)Moy6O`u1Gf7*^qq~q@5uQi z*unE5ya@&l+)H&~zX?+-apJW-yHyHE%uDFLO>Kf-jpO?o1E5;KV0p#aYYVt?MikX}W$B8T|F(&VhTR zP{J$VJ`B$|Q5?Srp6l>#hd(R;!*cj9A`p$hIL2cik*N;N2?=PzGjS5;PXs}uiS>yk zRWO^7TSBkICusVHi?r5_upSUaH!r~Y8*DDv8o<_`d(zwPgKZRS3k2QkVc0Ih?qx-DV!G-&Guo#YV_Ao^Xz2Wv$d+8)uk z^(EmItuzv?9)OL&lvKD|iHST9Pci!#G=-i&xF09etL{VKLw4_iHU#|ys3F(~!9fT< zh2T^KrwKOQH3+Up@CbsZ5%M6^385l{28-smBM^E5p~VQT;5bw06@=bH=sSeTLV5|| zn}}+RsBA>tiKwxNdKOW05cRz9RlY;iuZZp}%8phcdK2?C$KKC|Zv9HspMv^7BEAda z`yhTi68vcRCK8=Y^qrW2#1D{^grpWoiXdqTl7B-=SELjpr4lJ4k@7TB<{@Q;V1!JbjT*%sjtlyE{ z5!t)Y)P|Nry%!LqvqTK~Jp3OB;OjpGObFNz07b?`JUR~n z^21Ib@CpK-B0xd28wmU$mVY#YEfDO4U@3wRA(VnpM}!_mXfs0F5c&|IUr3cAY)7~~ z!hI2b2;mV3pG5c-CSU!Q;E{I1sxQ*3^mY38B5ZmQnh#n68)i)Q@II`!Rnn8KHTz1; zgEL8F0dE%&!9M1SBA~$bDQw@s-U9XqVJAyu8tn66U(6gl+;WHzpHH~0o&8TZf^fXa zogbYc!J$(I=VUlni(IPxB8U4gxPov2E($k|;6BkyM4#yBNN~2uLg+}Y(|eq_Z#pWd zPR%gq!E!AUV(uz3m&WLG_09So{TuxzC#%GWqO4T$7V|}F!vK+Bu#;IrOxFcuiEqkN%YyLgBF%9jNp=Ld#sIY*?v z?PJx*IWrwZvx&#KF_j)SQ!vf$*Pj=0^PPer>NHFgjE$cq=&L*FgJ@n)Ffo(|rZ{M$ z2*RI2_ESU7=4d!wIB#_Xp|DDLf>0<$=i8C#YkMioVi2Jpa}{J!NodVXMl5EqGnqt& z6VPkTYrM}A8y9S)O^ptc@)m~xMbEvSo7O(eJN7=yNO1YKg08v zz?u``&4f1}-a>dw;4OvsCC;z&-eL+>|MNl>>jGk?@M~WLo)B2`Nd)_fwmWAL`WB&| z5OoYO6(axOrYQe9gxGfw`xoL?A?`WEeUJG1h@XsvlSudh3ICu$IvTV_gU`|6Z!}Ct z!`4U)A~79FRY)3-q{B%12uZh*oPgw(NPY^*FCzI9B;OJZjv6DSGg8WtG6gA1k+Pd} z{Tn4CJp<{@k$yMQhai18(kCMGD>NolBpq4YygjBjVpcQfR_r#!zKABX(WH)Z6UdT_ z;(`d*op9YF43!vqHQ^Y9k0L3XEcy5J&$+>D=q1E0L&7ekld+jMCAU{FR#xXin;^WR z7|v5!DeH3uyg8R_D*lcg1X#a-tw`7$%{e1wChXsEK1!U* zN1?x6c*)f#^q?=vpL_I?qML^Z&~a|jlM%fa0|fi^lezasbJK=GS|`zZ6a685k^YzD z2xbTADSZ>;6tQB_O%n-jpF{gjWyP2u7AxEW%O1vE1~TcQo~IY+kLf4%m-V+L4})DW zD8~xdYObi$ex4Jhm{@+5AdmVL77dpAg7ooOSf&v!0W0BqM*L*Rb~{-Yw>p;(wG#}GV+;7GL!BW5Zbtm^uH>^bMgv zb41B`f5NV3>$Re*!#$!9q*lDxvRR@QBcCFYnwvz-!?YxrC`B)yEg;a5`Z$6=*AirU zlAyLXxZifgG6BynWeloP;Ae!Qk!$xjtPNltDrl}>BOjBZoxAx3T7DC@^)7Ng#ljmf oUFgDr`nS2;!g^XnucT>;6+cx;`l*j{w?+N`05p{Mh4|_K0K)cdY5)KL diff --git a/docs/public/search/index/zh-cn_cc28e9c.pf_index b/docs/public/search/index/zh-cn_cc28e9c.pf_index deleted file mode 100644 index a40f23bdd87bd2436a73e59f0ab70c59da5a5e06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37540 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 diff --git a/docs/public/search/index/zh-cn_dcdc9fa.pf_index b/docs/public/search/index/zh-cn_dcdc9fa.pf_index deleted file mode 100644 index ff0b051cbcb6f58449b18d8996c588c3ee7e3cfa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36103 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 WyegbbD0k)QU;RJ7wgaMe^8f%?WmAj* diff --git a/docs/public/search/pagefind-entry.json b/docs/public/search/pagefind-entry.json index b5a241265..18f117820 100644 --- a/docs/public/search/pagefind-entry.json +++ b/docs/public/search/pagefind-entry.json @@ -1 +1 @@ -{"version":"1.0.4","languages":{"zh-cn":{"hash":"zh-cn_ef5fdd1d36db6","wasm":null,"page_count":99},"no":{"hash":"no_4de94f6c5b","wasm":"no","page_count":3}}} \ No newline at end of file +{"version":"1.0.4","languages":{"zh-cn":{"hash":"zh-cn_2d3a7685f2714","wasm":null,"page_count":99},"no":{"hash":"no_4de94f6c5b","wasm":"no","page_count":3}}} \ No newline at end of file diff --git a/docs/public/search/pagefind.zh-cn_ef5fdd1d36db6.pf_meta b/docs/public/search/pagefind.zh-cn_ef5fdd1d36db6.pf_meta deleted file mode 100644 index d9a9f3fdfd1aa064a8844991ec39067cbb15a962..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 988 zcmV<210(z&iwFP!00002|4mi7tDQv@_ultHBq*W*nQ3gSA|!mXoS8YDjkSeKBrb90 z%$aD^Ac-51z#})26k2F3M2tctxF853Do+a=JN*kpuy}bI#HCP7nR&%`=C!~3%~^iS zxzFcg+@3yBk5;PZ*&Lb<`FH5-bEls=NGFc6sijfVi>(>8K`<3yuWz|$Mlxct8TId#Om=G zK+YVQ)WsQaF&DByJt*U5G&LFZwX{JK9kAa2(~LAqsew}u$wG~apNii1vOMy}1zXhN z8AvgQI3>MDX2X~x#;W(oieMu{2EFHiEbaOuB)xuF=5>w=RCQcFsSQl>>V4U{G`2PA zJ1@v5l&L1CPD-P`k!eA%|187U(1&S*C=+a8%zAlxMrx$$I=jp7Wfq1B*bW_$ih-2W zV*=zDTS$6&W=1@soyw%1kVaDm6T&D`bLX~!Uc9+W0YXmd{~54_0wt>#gQ!vfOH~g` zpTxN|7#e|d9;4HXG$Wxw%UzPSPo$BHwqexEqw=b_;4-Q!!*sO~S-tlrnHNKLG3e!D z3`5YhRZq$%rPqbN(n0A>(OaMO`W;d@TMNFZdxslZvJl~@8H=tH-O!Lemm;;=_|LKmfT_lI<9 z);ed0_RaJnlT(ij>qa0zy>?x8hBh_NdLeQ2F1F^p-gm2L4FP&^)R}PzdZ=rE^rMiO zyft0?uLQ6Ljjej|t8fR-IP8x3SgdJGHwdcN26xDaQEdn@4U+4JYbpK+-c`u`xDBae zOTE2b9vPwFQcMr}r$SyZnRC_ZF4^1e95+<2%6K_rfS#@ELpX5O8@>2<H&Gg$ObxF`$xWDq8#_X_spTbQSGwn!svkb#7`AX^s`2-W@RFxdX){4)>>cB`~ z;7mno/sitemap.xmlzh-cn/sitemap.xml2023-11-27T15:29:07+08:00 \ No newline at end of file + + + + + no/sitemap.xml + + + + + zh-cn/sitemap.xml + + 2023-12-19T11:12:43+08:00 + + + + diff --git a/docs/public/tags/index.html b/docs/public/tags/index.html index be36b02d5..b17b54fc6 100644 --- a/docs/public/tags/index.html +++ b/docs/public/tags/index.html @@ -1,7 +1,178 @@ -Tags | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +Tags | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      Tags

      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      +
      +

      Tags

      +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/tags/index.xml b/docs/public/tags/index.xml index 8a7e499f2..889964d93 100644 --- a/docs/public/tags/index.xml +++ b/docs/public/tags/index.xml @@ -1 +1,18 @@ -SOFAServerless – Tags/tags/Recent content in Tags on SOFAServerlessHugo -- gohugo.iozh-cn \ No newline at end of file + + + SOFAServerless – Tags + /tags/ + Recent content in Tags on SOFAServerless + Hugo -- gohugo.io + zh-cn + + + + + + + + + + + diff --git a/docs/public/user-cases/_print/index.html b/docs/public/user-cases/_print/index.html index f0adc0b35..890c4d009 100644 --- a/docs/public/user-cases/_print/index.html +++ b/docs/public/user-cases/_print/index.html @@ -1,12 +1,595 @@ -用户案例 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +用户案例 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      1 - 蚂蚁集团大规模 Serverless 降本增效实践

      作者:刘煜、赵真灵、刘晶、代巍、孙仁恩等

      蚂蚁集团业务痛点

      蚂蚁集团过去 20 年经历了微服务架构飞速演进,与此同时应用数量和复杂度爆发式增长,带来了严重的企业成本和效率问题:

      1. 大量长尾应用 CPU 使用率不足 10%,却由于多地域高可用消耗大量机器。
      2. 应用一次构建+发布速度慢,平均 10 分钟,研发效率低下且无法快速弹性
      3. 多人开发的应用,功能必须攒一起 “赶火车”,迭代互相阻塞,协作和交付效率低下。
      4. 业务 SDK 和部分框架升级对业务有较高打扰,无法做到基础设施对业务微感甚至无感。
      5. 业务资产难以沉淀,大中台建设成本高昂

      蚂蚁集团 SOFAServerless 使用场景

      合并部署降成本

      在企业中 “80%” 的长尾应用仅服务 “20%” 的流量,蚂蚁集团也不例外。
      在蚂蚁集团存在大量长尾应用,每个长尾应用至少需要 预发布、灰度、生产 3 个环境,每个环境最少需要部署 3 个机房,每个机房又必须保持 2 台机器高可用,因此大量长尾应用 CPU 使用率不足 10%
      通过使用 SOFAServerless,蚂蚁集团对长尾应用进行了服务器裁撤,借助类委托隔离、资源监控、日志监控等技术,在保证稳定性的前提下,实现了多应用的合并部署,极大降低了业务的运维成本和资源成本。

      此外,采用这种模式,小应用可以不走应用申请上线流程也不需要申请机器,可以直接部署到业务通用基座之上,从而帮助小流量业务实现了快速创新。

      模块化研发极致提效

      在蚂蚁集团,很多部门存在开发者人数较多的应用,由于人数多,导致环境抢占、联调抢占、测试资源抢占情况严重,互相阻塞,一人 Delay 多人 Delay,导致需求交付效率低下。
      通过使用 SOFAServerless,蚂蚁集团将协作人数较多的应用,一步步重构为基座代码和不同功能的模块代码。基座代码沉淀了各种 SDK 和业务的公共接口,由专人维护,而模块代码则内聚了某一个功能领域特有的业务逻辑,可以调用本地基座接口。模块采用热部署实现了十秒级构建、发布、伸缩,同时模块开发者完全不用关心服务器和基础设施,这样普通应用便以很低的接入成本实现了 Serverless 的研发体验。
      以蚂蚁集团资金业务为例,资金通过将应用拆分为一个基座与多个模块,实现了发布运维、组织协作、集群流量隔离多个维度的极致提效。

      蚂蚁集团资金业务 SOFAServerless 架构演进和实践,详见:https://mp.weixin.qq.com/s/uN0SyzkW_elYIwi03gis-Q

      通用基座屏蔽基础设施

      在蚂蚁集团,各种 SDK 的升级打扰、构建发布慢是痛点问题。借助 SOFAServerless 通用基座模式,蚂蚁集团帮助部分应用实现了基础设施微感升级,同时应用的构建与发布速度也从 600 秒减少到了 90 秒

      在 SOFAServerless 通用基座模式里,基座会提前启动好,这些预启动的基座包含了各种通用中间件、二方包和三方包的 SDK。借助 SOFAServerless 构建插件,业务应用会被构建成 FatJar 包,在发布业务应用新版本时,调度器会选择一台没有安装模块的空基座将模块应用 FatJar 热部署到基座,装有模块的老基座服务器会异步的替换成新服务器(空基座)。
      基座由专职团队负责维护和升级,对模块应用开发者来说,便享受到了基础设施无感升级和极速构建发布体验。

      低成本实现高效中台

      在蚂蚁集团,有不少中台类业务,典型如各个业务线的玩法、营销、公益、搜索推荐、广告投放等。通过使用 SOFAServerless,这些中台业务逐渐演进成了基座 + 模块的交付方式,其中基座代码沉淀了通用逻辑,也定义了一些 SPI,而模块负责实现这些 SPI,流量会从基座代码进入,调用模块的 SPI 实现。
      在中台场景下,模块一般都很轻,甚至只是一个代码片段,大部分模块都能在 5 秒内发布、扩容完成,而且模块开发者完全不关心基础设施,享受到了极致的 Serverless 研发体验。
      以蚂蚁集团搜索推荐业务中台为例,搜索推荐业务将公共依赖、通用逻辑、流程引擎全部下沉到基座,并且定义了一些 SPI,搜索推荐算法由各个模块开发者实现,当前搜索推荐已经接入了 1000+ 模块,平均代码发布上线不到 1 天,真正实现了代码的 “朝写夕发”。

      总结与规划

      SOFAServerless 在蚂蚁集团历经 3 年多的演进与打磨,目前已在所有业务线完成落地,支撑了全集团 1/4 的在线流量,帮助全集团实现了功能平均上线从 12 天减少到 6 天、长尾应用服务器砍半秒级发布运维的极致降本增效结果。
      未来,蚂蚁集团会更大规模推广 SOFAServerless 研发模式,并持续建设弹性能力,做到更极致的弹性体验和绿色低碳。同时,我们也会重点投入开源能力建设,希望和社区同学共同打造极致的模块化技术体系,为各行各业的软件创造技术价值,助力企业实现降本提效。

      2 - 阿里国际数字商业集团中台业务三倍提效

      作者: 朱林(封渊)、张建明(明门)

      项目背景

      过去几年,AIDC(阿里巴巴海外数字商业)业务板块在全球多个国家和地区拓展。国际电商业务模式上分为"跨境"和"本对本"两种,分别基于AE(跨境)、Lazada、Daraz、Mirivia、Trendyol 等多个电商平台承载。以下将不同的电商平台统称为“站点”。

      对于整个电商业务而言,各个站点核心的买卖家基础链路存在一定的差异性,但更多还是共性。通过抽象出一个通用的平台,在各个站点实现低成本复用有助于更高效的支持上层业务。所以过去几年基础链路一直在尝试通过平台化的建设思路,通过 1个全球化业务平台 + N个业务站点 的模式支持业务发展;其中技术的迭代经历了五个阶段的发展,从最初的中心化中台集成业务的架构模式,逐步转变为去中心化被业务集成的架构,已经基本可以满足全球化各个站点业务和平台各自闭环迭代。

      各个站点逻辑上是基于国际化中台(平台)做个性化定制,在交付/运维态各个站点被拆分成独立的应用,分别承载各自业务流量,平台能力通过二方包方式被站点应用集成,同时平台具备能力扩展机制,业务站点的研发能在站点应用中覆写平台逻辑,这最大化的保证了站点业务的研发/运维自主性,同时一定程度上保证了平台能力的复用性。 -但由于当前各个电商站点处于不同的发展时期,并且本对本跟跨境在业务模式上也存在差异性,以及业务策略上的不断变化,业务的快速迭代跟平台能力的后置沉淀逐步形成了矛盾,主要表现在如下几个方面:

      • 平台重复建设:由于平台采用开放、被集成的策略且缺乏一定的约束,需求的迭代即便是需要改动平台逻辑,站点也基本自闭环,在平台能力沉淀、稳定性、性能、开放性等各个站点都存在重复建设,支持不同站点的平台版本差异性逐步扩大;
      • 站点维护成本高:各自闭环的站点应用,由于同时维护了定制的平台能力,承担了部分"平台团队的职责",逐渐的增加了站点研发团队的负担,导致人力成本升高;
      • 研发迭代效率低:核心应用构建部署效率低下,以交易站点应用为例,系统启动时长稳定在 300s+,编译时长 150s+,镜像构建时长 30s+,容器重新初始化等调度层面的耗时 2min 左右,研发环境一天部署次数100+,如果能降低构建部署时长,将有效的降低研发等待时长;

      所以,下一代的架构迭代将需要重点解决平台去中心化被业务集成的架构模式下如何实现能力的迭代自主性以及版本统一,另外也需要考虑如何进一步降低站点的研发、运维成本,提升构建、部署效率,让业务研发真正只关注在自身的业务逻辑定制。 -Serverless 的技术理念中,强调关注点分离,让业务研发专注在业务逻辑的研发,而不太需要关注底层平台。这样一个理念,结合上述我们面临的问题或许是一个比较好的解法,让平台从二方包升级成为一个平台基座应用,统一收敛平台的迭代,包括应用运行时的升级;让业务站点应用轻量化,只关注定制逻辑的开发,提升部署效率,降低维护成本,整体逻辑架构图如下:

      概念阐述

      Serverless 普遍的理解是“无服务器架构”。它是云原生的主要技术之一,无服务器指的是用户不必关心应用运行和运维的管理,可以让用户去开发和使用应用程序,而不用去管理基础设施,云服务商提供、配置、管理用于运算的底层基础设施,将应用程序从基础结构中抽离出来,让开发人员可以更加专注于业务逻辑,从而提高交付能力,降低工作成本。传统 Serverless 在实现上其实就是 FaaS + BaaS。FaaS 承载代码片段(即函数),可随时随地创建、使用、销毁,无法自带状态。和 BaaS(后端即服务)搭配使用。两者合在一起,才最终实现了完整行为的 Serverless 服务。

      在传统 Serverless 技术体系下,Java 应用架构更多的是解决了 IaaS 层 + 容器化 的问题,Serverless 本身无法将涵盖范围下探到 JVM 内部。因此,对于一个复杂的 Java 巨石应用,可以借助 Serverless 的理念将 Java 技术栈下的业务代码和基础设施(中间件)依赖做更进一步的剥离与拆分。本次实践中的 Serverless 改造可以抽象为如下过程和目标:

      将一个单体应用横向拆分成上下两层:

      • 基座:将一些业务应用迭代中不经常变更的基础组件以及 Lib 包下沉到一个新的应用中去,这个新应用我们称之为 基座应用,有以下特点:
        • 基座是可独立发布与运维的
        • 基座应用开发者可以统一升级中间件和组件版本,在保证兼容性的前提下,上层 App 无需感知整个升级过程
        • 基座具备不同站点间的可复用性,一个交易的基座可以被 AE、Lazada、Daraz等不同的站点 App 共用
      • Serverless App:为了最大程度减少业务的改造成本,业务团队维护的 App 依旧保持其自身的发布运维职责的独立性不变,Serverless 化后业务开发人员只需要关心业务代码即可。JVM 对外服务的载体依旧是业务应用。

      技术实施

      Serverless 架构演技的实施过程分为两个部分:

      1. 重新设计 Serverless 架构模型下的应用架构分层与职责划分,让业务减负,让 SRE 提效。
      2. 在研发、发布&运维等领域采用新的研发框架、交付模型与发布运维产品来支持业务快速迭代。

      应用架构

      以 Daraz 基础链路为例,应用架构的分层结构、交互关系、团队职责如下:

      我们将一个 Serverless 应用完整交付所需的支撑架构进行逻辑分层与开发职责划分,并且清晰定义出 App 与基座之间交互的协议标准。

      研发域

      • 构建了 Serverless 运行时框架,驱动"基座-Serverless App"的运行与交互
      • 与 Aone 研发平台团队协作,建设一整套 Serverless 模式下基座与 App 的发布&运维产品体系

      voyager-serverless framework

      voyager-serverless framework 是一个基于 SOFAServerless 技术自研的提供 Serverless编程界面 的研发框架,允许业务 App 以 动态 的方式被装载到一个正在运行的基座容器(ArkContainer)中。在 SOFAServerless 模块隔离能力的基础上,我们针对阿里集团技术栈做了深度改造定制。

      整套框架提供以下关键能力:

      隔离性与运行时原理

      框架实现了基座与应用模块的 ClassLoader隔离SpringContext隔离。启动流程上,一共分为 两阶三步,启动顺序自底向上:

      • 一阶段基座启动
        • 第一步: 启动 Bootstrap,包含 Kondyle 以及 Pandora 容器,加载 Kondyle 插件 以及 Pandora 中间件插件类或对象
        • 第二步: 基座应用启动,其内部顺序为
          • 启动 ArkContainer,初始化 Serverless 相关组件
          • 基座应用 SpringContext 初始化,对象初始化过程中加载 基座自有类、基座Plugin类、依赖包类、中间件SDK类
      • 二阶段app启动
        • 第三步: Serverless App启动,由 ArkContainer 内置组件接受 Fiber 调度请求下载 App 制品并触发 App Launch
          • 创建 BizClassLoader,作为线程 ClassLoader 初始化 SpringContext,加载 App自有类、基座Plugin类、依赖包类、中间件SDK类

      通信机制

      在 Serverless 形态下,基座与 App 之间可以通过 进程内通信方式 进行交互,目前提供两种通信模型: SPI 基座Bean服务导出

      SPI 本质上就是基于标准 Java SPI 的扩展的内部特殊实现,本文不额外赘述,这里重点介绍下 基座Bean服务导出

      一般情况下,基座的 SpringContext 与 App 的 SpringContext 是完全隔离的,且没有父子继承关系。因此 App 侧不能通过常规 @Autowired 的方式引用下层基座 SpringContext 中的 Bean。 -除了 Class 的下沉,在一些场景下,基座可以将自己已经初始化好的 Bean 对象也下沉掉,声明并暴露给上层 App 使用。这样之后 App 启动的时候可以直接拿到基座 SpringContext 中的已经完成初始化的 Bean,用以加快 App 的启动速度。其过程如下:

      1. 用户通过配置或者注解标注声明需要在基座中导出的 Bean 服务
      2. 基座启动结束后,隔离容器会自动将用户标注的 Bean 导出到一个缓冲区中,等待 App 的启动使用
      3. App 在基座上启动,App 的 SpringContext 初始化过程中,会在初始化阶段读取到缓冲区中需要导入的 Bean
      4. App 的 SpringContext 中的其他组件可以正常 @Autowired 这些导出的 Bean

      插件机制

      Serverless 插件提供了一种能够让 App 运行时所需的类默认从基座中加载的机制,框架支持将平台基座需要暴露给上层 App 使用的SDK/二方包等包装成一个插件(Ark Plugin),最终实现中台控制的包下沉到基座而不需要让上层业务改动:

      中间件适配

      在 Serverless 架构演进中,由于一个完整应用的启动过程被拆分成基座启动和 App 启动,因此在一二阶段相关中间件的初始化逻辑也发生了变化。我们对国际侧常用的中间件和产品组件进行了测试,并对部分中间件进行了适配改造。 -总结起来,大部分问题都出现在中间件一些流程逻辑没设计这种多 ClassLoader 的场景,很多类/方法使用中不会将ClassLoader 对象作为参数传递进来,进而在初始化模型对象时出错,导致上下文交互异常。

      开发配套

      我们也提供了一整套完整且简单易用的配套工具,方便开发者快速接入 Serverless 体系:

      发布 & 运维域

      除了研发域,Serverless 架构在发布运维领域也带来很多新的变化。首先是研发运维分层拆分,实现了关注点分离,降低研发复杂度:

      • 逻辑拆分:将原本应用拆分,将业务代码和基础设施隔离,基础能力下沉,比如将改造前启动过程中耗时中间件、一些富二方库和需要管控的二方库等等下沉到基座中,实现了业务应用轻量化。
      • 独立演进:分层拆分后,基座和业务应用各自迭代,SRE 可以在基座上将基础设施的统一管控和升级,较少甚至杜绝了业务的升级成本。

      我们也和 Aone 一起合作,voyager-serverless 借助 OSR(Open Serverless Runtime) 标准协议接入 Aone Serverless 产品技术体系中去。借助新的发布模型和部署策略,在 App 构建速度和启动效率上得到很大提升。

      构建效率提升

      • Maven 构建优化: 由于很多依赖包都已经下沉到已经就绪的基座,因此对于 App 来说就可以减少需要构建的二方包数量以及 Class 文件数量,进而整体优化制品大小,提升构建效率
      • Docker 构建移除: 由于 Serverless 模式下业务 App 部署的制品就是轻量化 Fat Jar,因此也无需进行 docker 构建

      发布效率提升

      在 Serverless 模式下,我们采用 Surge+流式发布 替换传统的分批发布来进一步提升 App 的发布效率。

      名词描述
      分批发布   分批发布策略是达到每个批次的新节点后,进行下一批次,如 100 个节点,10 个批次,第一批次卡点 10 个新节点,第二批次卡点 20 个新节点,依次类推
      Surge   Surge 发布策略可以在不影响业务可用性的前提下加速业务发布效率:
      1) 发布时会新增加配置 Surge 比例的数量节点,比如业务有 10 台机器,Surge 配置百分比为 50,发布过程就会首先增加 5 台机器用于发布
      2) 如果基座中配置了合理大小的 Buffer,那么可以直接从 Buffer 中获取这 5 台机器,直接发布新版本代码
      3) 整体新版本节点数达到预期数量(本例中 10 台机器),直接下线旧节点,完成整次发布过程
      其中 Surge 结合流式发布一起使用,同时配合 Runtime 中合适数量的 Buffer,可以最大程度地加速业务发布效率
      • 瀑布流分批发布:每一个批次的机器全部发布上线之后开始发布下一批次,批次内机器并行发布,批次之间串行。假设有100台机器,分10批次发布,每批发布的机器数为10台,发布总耗时为:

      • Surge流式发布:发布过程中,允许多分配一些机器来参与更新,核心原理为 满足可用度的前提 下,增大单轮次中参与更新的机器个数。例如有100台机器,在满足可用度≥90%的前提下,即任意时刻至少有90(100*90%)个机器在线,surge=5%的发布调度过程如下:

      借助这个新的发布模型,我们在开发变更最频繁的日常和预发环境全面开启 Surge 发布,用来加速业务 App 的发布。

      • 在进行 Serverless 改造前:
        • 为了保证发布过程中流量不受影响,一般情况下,一个预发环境会保留两台机器(replica = 2),执行传统的分批发布(batch=2),也就是每台机器轮流更新。
        • 这里我们假定应用启动耗时为5min,其中频繁变更的业务代码1min,平台以及中间件等组件加载耗时4min
        • 发布总耗时为 5min + 5min = 10min

      • 完成 Serverless 改造,采用 Surge 流式发布后
        • App 的预发环境只需要保留一台机器(replica = 1),基座设置buffer = 1,即保留一台空基座用于准备给 App 调度使用
        • 发布策略上,App 环境 Surge 比例为100%
        • 由于只发布更新了 App 的 Biz 代码,发布总耗时为 1min,并且整个过程机器总成本保持不变

      同时,我们也在生产环境配置一定数量的基座 Buffer,支持站点 App 的快速弹性扩容。

      总结与展望

      目前已经完成 Daraz 业务站点的交易、营销、履约、会员、逆向应用的 Serverless 升级改造,在 构建时长单机应用启动时长预发环境发布时长 三个指标上均取得了较大优化效果。在个别领域,甚至真正做到了 App 的 10秒 级启动。

      可以看到,本次 Serverless 架构的升级,无论从理论推演还是实践结果,都产生了较大的正向收益与效率提升,这给后续业务 App 的快速迭代带来了不少便利。同时,由于平台代码下沉为基座应用,也具备了跟业务站点正交的发布能力,基本上可以实现基础链路平台版本统一的目标;“关注点分离"也解放了业务开发者,让他们更多关注在自己的业务代码上。但是,还有一些如开发配套成熟度、问题定位与诊断、生产环境成本最优的基座配置等问题与挑战需要进一步解决。我们也会深度参与共建 SOFAServerless 开源社区,发掘更多的落地场景与实践经验。

      Serverless 从来不是某个单一的架构形态,它带来的更多是一种理念和生产方式。理解它、利用它,帮助我们拓宽新的思路与解题方法。

      3 - SOFAServerless 所有企业案例

      目前主动登记使用 SOFAServerless 的企业,按企业拼音字母顺序排序,不分先后:


      阿里国际数字商业集团

      使用场景:通用基座实现普通应用秒级构建发布、SDK 无感升级。详细案例



      阿里健康科技(中国)有限公司

      https://www.alihealth.cn/

      使用场景:应用插件化开发



      阿里妈妈

      https://www.alihealth.cn/

      使用场景:合并部署、热部署



      斑马信息科技有限公司

      https://www.ebanma.com/

      使用场景:类隔离、合并部署



      成都云智天下科技股份有限公司

      https://www.yunzhitx.com/

      使用场景:模块动态部署、卸载



      华信永道(北京)科技股份有限公司

      使用场景:多应用合并部署降本增效

      https://www.yondervision.com.cn/



      蚂蚁集团

      https://www.antgroup.com/

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署,实现资源降本和研发提效。详细案例



      南京爱福路汽车科技有限公司

      https://www.f6car.cn/

      使用场景:多应用合并部署降本增效



      深圳市诺安赛威科技有限公司

      https://company.rfidworld.com.cn/Intro-137503.aspx

      使用场景:动态热部署、热卸载、模块隔离



      上海一嗨信息技术服务有限公司

      https://www.1hai.cn/

      使用场景:多应用合并部署降本增效、模块热部署实现秒级构建发布



      山东网聪信息科技有限公司

      https://www.gridnt.cn/

      使用场景:类隔离



      宋城独木桥网络有限公司

      https://www.dmqwl.com/

      使用场景:动态热部署、热卸载、模块隔离



      网商银行

      https://www.mybank.cn/

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署



      易宝支付有限公司

      https://www.yeepay.com/

      使用场景:项目插件化,如动态部署、卸载、模块隔离



      纵目科技(上海)股份有限公司

      https://www.zongmutech.com/

      使用场景:多应用合并部署降本增效


      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      +
      + + + + + +
      +
      +

      +这是本节的多页打印视图。 +点击此处打印. +

      +返回本页常规视图. +

      +
      + + + +

      用户案例

      + + + + + + + + +
      + +
      +
      + + + + + + + + + + + + + + + + +
      + +

      1 - 蚂蚁集团大规模 Serverless 降本增效实践

      + +
      +

      作者:刘煜、赵真灵、刘晶、代巍、孙仁恩等

      +
      +

      蚂蚁集团业务痛点

      +

      蚂蚁集团过去 20 年经历了微服务架构飞速演进,与此同时应用数量和复杂度爆发式增长,带来了严重的企业成本和效率问题:

      +
        +
      1. 大量长尾应用 CPU 使用率不足 10%,却由于多地域高可用消耗大量机器。
      2. +
      3. 应用一次构建+发布速度慢,平均 10 分钟,研发效率低下且无法快速弹性
      4. +
      5. 多人开发的应用,功能必须攒一起 “赶火车”,迭代互相阻塞,协作和交付效率低下。
      6. +
      7. 业务 SDK 和部分框架升级对业务有较高打扰,无法做到基础设施对业务微感甚至无感。
      8. +
      9. 业务资产难以沉淀,大中台建设成本高昂
      10. +
      +

      蚂蚁集团 SOFAServerless 使用场景

      +

      合并部署降成本

      +

      在企业中 “80%” 的长尾应用仅服务 “20%” 的流量,蚂蚁集团也不例外。
      在蚂蚁集团存在大量长尾应用,每个长尾应用至少需要 预发布、灰度、生产 3 个环境,每个环境最少需要部署 3 个机房,每个机房又必须保持 2 台机器高可用,因此大量长尾应用 CPU 使用率不足 10%
      通过使用 SOFAServerless,蚂蚁集团对长尾应用进行了服务器裁撤,借助类委托隔离、资源监控、日志监控等技术,在保证稳定性的前提下,实现了多应用的合并部署,极大降低了业务的运维成本和资源成本。

      此外,采用这种模式,小应用可以不走应用申请上线流程也不需要申请机器,可以直接部署到业务通用基座之上,从而帮助小流量业务实现了快速创新。

      +

      模块化研发极致提效

      +

      在蚂蚁集团,很多部门存在开发者人数较多的应用,由于人数多,导致环境抢占、联调抢占、测试资源抢占情况严重,互相阻塞,一人 Delay 多人 Delay,导致需求交付效率低下。
      通过使用 SOFAServerless,蚂蚁集团将协作人数较多的应用,一步步重构为基座代码和不同功能的模块代码。基座代码沉淀了各种 SDK 和业务的公共接口,由专人维护,而模块代码则内聚了某一个功能领域特有的业务逻辑,可以调用本地基座接口。模块采用热部署实现了十秒级构建、发布、伸缩,同时模块开发者完全不用关心服务器和基础设施,这样普通应用便以很低的接入成本实现了 Serverless 的研发体验。
      以蚂蚁集团资金业务为例,资金通过将应用拆分为一个基座与多个模块,实现了发布运维、组织协作、集群流量隔离多个维度的极致提效。

      +

      蚂蚁集团资金业务 SOFAServerless 架构演进和实践,详见:https://mp.weixin.qq.com/s/uN0SyzkW_elYIwi03gis-Q

      +

      通用基座屏蔽基础设施

      +

      在蚂蚁集团,各种 SDK 的升级打扰、构建发布慢是痛点问题。借助 SOFAServerless 通用基座模式,蚂蚁集团帮助部分应用实现了基础设施微感升级,同时应用的构建与发布速度也从 600 秒减少到了 90 秒

      +

      在 SOFAServerless 通用基座模式里,基座会提前启动好,这些预启动的基座包含了各种通用中间件、二方包和三方包的 SDK。借助 SOFAServerless 构建插件,业务应用会被构建成 FatJar 包,在发布业务应用新版本时,调度器会选择一台没有安装模块的空基座将模块应用 FatJar 热部署到基座,装有模块的老基座服务器会异步的替换成新服务器(空基座)。
      基座由专职团队负责维护和升级,对模块应用开发者来说,便享受到了基础设施无感升级和极速构建发布体验。

      +

      低成本实现高效中台

      +

      在蚂蚁集团,有不少中台类业务,典型如各个业务线的玩法、营销、公益、搜索推荐、广告投放等。通过使用 SOFAServerless,这些中台业务逐渐演进成了基座 + 模块的交付方式,其中基座代码沉淀了通用逻辑,也定义了一些 SPI,而模块负责实现这些 SPI,流量会从基座代码进入,调用模块的 SPI 实现。
      在中台场景下,模块一般都很轻,甚至只是一个代码片段,大部分模块都能在 5 秒内发布、扩容完成,而且模块开发者完全不关心基础设施,享受到了极致的 Serverless 研发体验。
      以蚂蚁集团搜索推荐业务中台为例,搜索推荐业务将公共依赖、通用逻辑、流程引擎全部下沉到基座,并且定义了一些 SPI,搜索推荐算法由各个模块开发者实现,当前搜索推荐已经接入了 1000+ 模块,平均代码发布上线不到 1 天,真正实现了代码的 “朝写夕发”。

      +

      总结与规划

      +

      SOFAServerless 在蚂蚁集团历经 3 年多的演进与打磨,目前已在所有业务线完成落地,支撑了全集团 1/4 的在线流量,帮助全集团实现了功能平均上线从 12 天减少到 6 天、长尾应用服务器砍半秒级发布运维的极致降本增效结果。
      未来,蚂蚁集团会更大规模推广 SOFAServerless 研发模式,并持续建设弹性能力,做到更极致的弹性体验和绿色低碳。同时,我们也会重点投入开源能力建设,希望和社区同学共同打造极致的模块化技术体系,为各行各业的软件创造技术价值,助力企业实现降本提效。

      + +
      + + + + + + + + + + + +
      + +

      2 - 阿里国际数字商业集团中台业务三倍提效

      + +
      +

      作者: 朱林(封渊)、张建明(明门)

      +
      +

      项目背景

      +

      过去几年,AIDC(阿里巴巴海外数字商业)业务板块在全球多个国家和地区拓展。国际电商业务模式上分为"跨境"和"本对本"两种,分别基于AE(跨境)、Lazada、Daraz、Mirivia、Trendyol 等多个电商平台承载。以下将不同的电商平台统称为“站点”。

      +

      +

      对于整个电商业务而言,各个站点核心的买卖家基础链路存在一定的差异性,但更多还是共性。通过抽象出一个通用的平台,在各个站点实现低成本复用有助于更高效的支持上层业务。所以过去几年基础链路一直在尝试通过平台化的建设思路,通过 1个全球化业务平台 + N个业务站点 的模式支持业务发展;其中技术的迭代经历了五个阶段的发展,从最初的中心化中台集成业务的架构模式,逐步转变为去中心化被业务集成的架构,已经基本可以满足全球化各个站点业务和平台各自闭环迭代。

      +

      +

      各个站点逻辑上是基于国际化中台(平台)做个性化定制,在交付/运维态各个站点被拆分成独立的应用,分别承载各自业务流量,平台能力通过二方包方式被站点应用集成,同时平台具备能力扩展机制,业务站点的研发能在站点应用中覆写平台逻辑,这最大化的保证了站点业务的研发/运维自主性,同时一定程度上保证了平台能力的复用性。 +但由于当前各个电商站点处于不同的发展时期,并且本对本跟跨境在业务模式上也存在差异性,以及业务策略上的不断变化,业务的快速迭代跟平台能力的后置沉淀逐步形成了矛盾,主要表现在如下几个方面:

      +
        +
      • 平台重复建设:由于平台采用开放、被集成的策略且缺乏一定的约束,需求的迭代即便是需要改动平台逻辑,站点也基本自闭环,在平台能力沉淀、稳定性、性能、开放性等各个站点都存在重复建设,支持不同站点的平台版本差异性逐步扩大;
      • +
      • 站点维护成本高:各自闭环的站点应用,由于同时维护了定制的平台能力,承担了部分"平台团队的职责",逐渐的增加了站点研发团队的负担,导致人力成本升高;
      • +
      • 研发迭代效率低:核心应用构建部署效率低下,以交易站点应用为例,系统启动时长稳定在 300s+,编译时长 150s+,镜像构建时长 30s+,容器重新初始化等调度层面的耗时 2min 左右,研发环境一天部署次数100+,如果能降低构建部署时长,将有效的降低研发等待时长;
      • +
      +

      所以,下一代的架构迭代将需要重点解决平台去中心化被业务集成的架构模式下如何实现能力的迭代自主性以及版本统一,另外也需要考虑如何进一步降低站点的研发、运维成本,提升构建、部署效率,让业务研发真正只关注在自身的业务逻辑定制。 +Serverless 的技术理念中,强调关注点分离,让业务研发专注在业务逻辑的研发,而不太需要关注底层平台。这样一个理念,结合上述我们面临的问题或许是一个比较好的解法,让平台从二方包升级成为一个平台基座应用,统一收敛平台的迭代,包括应用运行时的升级;让业务站点应用轻量化,只关注定制逻辑的开发,提升部署效率,降低维护成本,整体逻辑架构图如下:

      +

      +

      概念阐述

      +

      Serverless 普遍的理解是“无服务器架构”。它是云原生的主要技术之一,无服务器指的是用户不必关心应用运行和运维的管理,可以让用户去开发和使用应用程序,而不用去管理基础设施,云服务商提供、配置、管理用于运算的底层基础设施,将应用程序从基础结构中抽离出来,让开发人员可以更加专注于业务逻辑,从而提高交付能力,降低工作成本。传统 Serverless 在实现上其实就是 FaaS + BaaS。FaaS 承载代码片段(即函数),可随时随地创建、使用、销毁,无法自带状态。和 BaaS(后端即服务)搭配使用。两者合在一起,才最终实现了完整行为的 Serverless 服务。

      +

      +

      在传统 Serverless 技术体系下,Java 应用架构更多的是解决了 IaaS 层 + 容器化 的问题,Serverless 本身无法将涵盖范围下探到 JVM 内部。因此,对于一个复杂的 Java 巨石应用,可以借助 Serverless 的理念将 Java 技术栈下的业务代码和基础设施(中间件)依赖做更进一步的剥离与拆分。本次实践中的 Serverless 改造可以抽象为如下过程和目标:

      +

      +

      将一个单体应用横向拆分成上下两层:

      +
        +
      • 基座:将一些业务应用迭代中不经常变更的基础组件以及 Lib 包下沉到一个新的应用中去,这个新应用我们称之为 基座应用,有以下特点: +
          +
        • 基座是可独立发布与运维的
        • +
        • 基座应用开发者可以统一升级中间件和组件版本,在保证兼容性的前提下,上层 App 无需感知整个升级过程
        • +
        • 基座具备不同站点间的可复用性,一个交易的基座可以被 AE、Lazada、Daraz等不同的站点 App 共用
        • +
        +
      • +
      • Serverless App:为了最大程度减少业务的改造成本,业务团队维护的 App 依旧保持其自身的发布运维职责的独立性不变,Serverless 化后业务开发人员只需要关心业务代码即可。JVM 对外服务的载体依旧是业务应用。
      • +
      +

      技术实施

      +

      +

      Serverless 架构演技的实施过程分为两个部分:

      +
        +
      1. 重新设计 Serverless 架构模型下的应用架构分层与职责划分,让业务减负,让 SRE 提效。
      2. +
      3. 在研发、发布&运维等领域采用新的研发框架、交付模型与发布运维产品来支持业务快速迭代。
      4. +
      +

      应用架构

      +

      以 Daraz 基础链路为例,应用架构的分层结构、交互关系、团队职责如下:

      +

      +

      我们将一个 Serverless 应用完整交付所需的支撑架构进行逻辑分层与开发职责划分,并且清晰定义出 App 与基座之间交互的协议标准。

      +

      研发域

      +

      +
        +
      • 构建了 Serverless 运行时框架,驱动"基座-Serverless App"的运行与交互
      • +
      • 与 Aone 研发平台团队协作,建设一整套 Serverless 模式下基座与 App 的发布&运维产品体系
      • +
      +

      voyager-serverless framework

      +

      +

      voyager-serverless framework 是一个基于 SOFAServerless 技术自研的提供 Serverless编程界面 的研发框架,允许业务 App 以 动态 的方式被装载到一个正在运行的基座容器(ArkContainer)中。在 SOFAServerless 模块隔离能力的基础上,我们针对阿里集团技术栈做了深度改造定制。

      +

      整套框架提供以下关键能力:

      +

      +

      隔离性与运行时原理

      +

      +

      框架实现了基座与应用模块的 ClassLoader隔离SpringContext隔离。启动流程上,一共分为 两阶三步,启动顺序自底向上:

      +
        +
      • 一阶段基座启动 +
          +
        • 第一步: 启动 Bootstrap,包含 Kondyle 以及 Pandora 容器,加载 Kondyle 插件 以及 Pandora 中间件插件类或对象
        • +
        • 第二步: 基座应用启动,其内部顺序为 +
            +
          • 启动 ArkContainer,初始化 Serverless 相关组件
          • +
          • 基座应用 SpringContext 初始化,对象初始化过程中加载 基座自有类、基座Plugin类、依赖包类、中间件SDK类
          • +
          +
        • +
        +
      • +
      • 二阶段app启动 +
          +
        • 第三步: Serverless App启动,由 ArkContainer 内置组件接受 Fiber 调度请求下载 App 制品并触发 App Launch +
            +
          • 创建 BizClassLoader,作为线程 ClassLoader 初始化 SpringContext,加载 App自有类、基座Plugin类、依赖包类、中间件SDK类
          • +
          +
        • +
        +
      • +
      +

      通信机制

      +

      在 Serverless 形态下,基座与 App 之间可以通过 进程内通信方式 进行交互,目前提供两种通信模型: SPI 基座Bean服务导出

      +
      +

      SPI 本质上就是基于标准 Java SPI 的扩展的内部特殊实现,本文不额外赘述,这里重点介绍下 基座Bean服务导出

      +
      +

      一般情况下,基座的 SpringContext 与 App 的 SpringContext 是完全隔离的,且没有父子继承关系。因此 App 侧不能通过常规 @Autowired 的方式引用下层基座 SpringContext 中的 Bean。 +除了 Class 的下沉,在一些场景下,基座可以将自己已经初始化好的 Bean 对象也下沉掉,声明并暴露给上层 App 使用。这样之后 App 启动的时候可以直接拿到基座 SpringContext 中的已经完成初始化的 Bean,用以加快 App 的启动速度。其过程如下:

      +

      +
        +
      1. 用户通过配置或者注解标注声明需要在基座中导出的 Bean 服务
      2. +
      3. 基座启动结束后,隔离容器会自动将用户标注的 Bean 导出到一个缓冲区中,等待 App 的启动使用
      4. +
      5. App 在基座上启动,App 的 SpringContext 初始化过程中,会在初始化阶段读取到缓冲区中需要导入的 Bean
      6. +
      7. App 的 SpringContext 中的其他组件可以正常 @Autowired 这些导出的 Bean
      8. +
      +

      插件机制

      +

      Serverless 插件提供了一种能够让 App 运行时所需的类默认从基座中加载的机制,框架支持将平台基座需要暴露给上层 App 使用的SDK/二方包等包装成一个插件(Ark Plugin),最终实现中台控制的包下沉到基座而不需要让上层业务改动:

      +

      +

      中间件适配

      +

      在 Serverless 架构演进中,由于一个完整应用的启动过程被拆分成基座启动和 App 启动,因此在一二阶段相关中间件的初始化逻辑也发生了变化。我们对国际侧常用的中间件和产品组件进行了测试,并对部分中间件进行了适配改造。 +总结起来,大部分问题都出现在中间件一些流程逻辑没设计这种多 ClassLoader 的场景,很多类/方法使用中不会将ClassLoader 对象作为参数传递进来,进而在初始化模型对象时出错,导致上下文交互异常。

      +

      开发配套

      +

      我们也提供了一整套完整且简单易用的配套工具,方便开发者快速接入 Serverless 体系:

      +

      +

      发布 & 运维域

      +

      除了研发域,Serverless 架构在发布运维领域也带来很多新的变化。首先是研发运维分层拆分,实现了关注点分离,降低研发复杂度:

      +

      +
        +
      • 逻辑拆分:将原本应用拆分,将业务代码和基础设施隔离,基础能力下沉,比如将改造前启动过程中耗时中间件、一些富二方库和需要管控的二方库等等下沉到基座中,实现了业务应用轻量化。
      • +
      • 独立演进:分层拆分后,基座和业务应用各自迭代,SRE 可以在基座上将基础设施的统一管控和升级,较少甚至杜绝了业务的升级成本。
      • +
      +

      +

      我们也和 Aone 一起合作,voyager-serverless 借助 OSR(Open Serverless Runtime) 标准协议接入 Aone Serverless 产品技术体系中去。借助新的发布模型和部署策略,在 App 构建速度和启动效率上得到很大提升。

      +

      +

      构建效率提升

      +
        +
      • Maven 构建优化: 由于很多依赖包都已经下沉到已经就绪的基座,因此对于 App 来说就可以减少需要构建的二方包数量以及 Class 文件数量,进而整体优化制品大小,提升构建效率
      • +
      • Docker 构建移除: 由于 Serverless 模式下业务 App 部署的制品就是轻量化 Fat Jar,因此也无需进行 docker 构建
      • +
      +

      发布效率提升

      +

      在 Serverless 模式下,我们采用 Surge+流式发布 替换传统的分批发布来进一步提升 App 的发布效率。

      + + + + + + + + + + + + + + + + + +
      名词描述
      分批发布   分批发布策略是达到每个批次的新节点后,进行下一批次,如 100 个节点,10 个批次,第一批次卡点 10 个新节点,第二批次卡点 20 个新节点,依次类推
      Surge   Surge 发布策略可以在不影响业务可用性的前提下加速业务发布效率:
      1) 发布时会新增加配置 Surge 比例的数量节点,比如业务有 10 台机器,Surge 配置百分比为 50,发布过程就会首先增加 5 台机器用于发布
      2) 如果基座中配置了合理大小的 Buffer,那么可以直接从 Buffer 中获取这 5 台机器,直接发布新版本代码
      3) 整体新版本节点数达到预期数量(本例中 10 台机器),直接下线旧节点,完成整次发布过程
      其中 Surge 结合流式发布一起使用,同时配合 Runtime 中合适数量的 Buffer,可以最大程度地加速业务发布效率
      +
        +
      • 瀑布流分批发布:每一个批次的机器全部发布上线之后开始发布下一批次,批次内机器并行发布,批次之间串行。假设有100台机器,分10批次发布,每批发布的机器数为10台,发布总耗时为:
      • +
      +

      +
        +
      • Surge流式发布:发布过程中,允许多分配一些机器来参与更新,核心原理为 满足可用度的前提 下,增大单轮次中参与更新的机器个数。例如有100台机器,在满足可用度≥90%的前提下,即任意时刻至少有90(100*90%)个机器在线,surge=5%的发布调度过程如下:
      • +
      +

      +

      +

      借助这个新的发布模型,我们在开发变更最频繁的日常和预发环境全面开启 Surge 发布,用来加速业务 App 的发布。

      +
        +
      • 在进行 Serverless 改造前: +
          +
        • 为了保证发布过程中流量不受影响,一般情况下,一个预发环境会保留两台机器(replica = 2),执行传统的分批发布(batch=2),也就是每台机器轮流更新。
        • +
        • 这里我们假定应用启动耗时为5min,其中频繁变更的业务代码1min,平台以及中间件等组件加载耗时4min
        • +
        • 发布总耗时为 5min + 5min = 10min
        • +
        +
      • +
      +

      +
        +
      • 完成 Serverless 改造,采用 Surge 流式发布后 +
          +
        • App 的预发环境只需要保留一台机器(replica = 1),基座设置buffer = 1,即保留一台空基座用于准备给 App 调度使用
        • +
        • 发布策略上,App 环境 Surge 比例为100%
        • +
        • 由于只发布更新了 App 的 Biz 代码,发布总耗时为 1min,并且整个过程机器总成本保持不变
        • +
        +
      • +
      +

      +

      同时,我们也在生产环境配置一定数量的基座 Buffer,支持站点 App 的快速弹性扩容。

      +

      总结与展望

      +

      目前已经完成 Daraz 业务站点的交易、营销、履约、会员、逆向应用的 Serverless 升级改造,在 构建时长单机应用启动时长预发环境发布时长 三个指标上均取得了较大优化效果。在个别领域,甚至真正做到了 App 的 10秒 级启动。

      +

      +

      可以看到,本次 Serverless 架构的升级,无论从理论推演还是实践结果,都产生了较大的正向收益与效率提升,这给后续业务 App 的快速迭代带来了不少便利。同时,由于平台代码下沉为基座应用,也具备了跟业务站点正交的发布能力,基本上可以实现基础链路平台版本统一的目标;“关注点分离"也解放了业务开发者,让他们更多关注在自己的业务代码上。但是,还有一些如开发配套成熟度、问题定位与诊断、生产环境成本最优的基座配置等问题与挑战需要进一步解决。我们也会深度参与共建 SOFAServerless 开源社区,发掘更多的落地场景与实践经验。

      +

      Serverless 从来不是某个单一的架构形态,它带来的更多是一种理念和生产方式。理解它、利用它,帮助我们拓宽新的思路与解题方法。

      + +
      + + + + + + + + + + + +
      + +

      3 - SOFAServerless 所有企业案例

      + +

      目前主动登记使用 SOFAServerless 的企业,按企业拼音字母顺序排序,不分先后:

      +
      +

      阿里国际数字商业集团

      + +

      使用场景:通用基座实现普通应用秒级构建发布、SDK 无感升级。详细案例

      +
      +
      +

      阿里健康科技(中国)有限公司

      + +

      https://www.alihealth.cn/

      +

      使用场景:应用插件化开发

      +
      +
      +

      阿里妈妈

      + +

      https://www.alihealth.cn/

      +

      使用场景:合并部署、热部署

      +
      +
      +

      斑马信息科技有限公司

      + +

      https://www.ebanma.com/

      +

      使用场景:类隔离、合并部署

      +
      +
      +

      成都云智天下科技股份有限公司

      + +

      https://www.yunzhitx.com/

      +

      使用场景:模块动态部署、卸载

      +
      +
      +

      华信永道(北京)科技股份有限公司

      +

      使用场景:多应用合并部署降本增效

      + +

      https://www.yondervision.com.cn/

      +
      +
      +

      蚂蚁集团

      + +

      https://www.antgroup.com/

      +

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署,实现资源降本和研发提效。详细案例

      +
      +
      +

      南京爱福路汽车科技有限公司

      + +

      https://www.f6car.cn/

      +

      使用场景:多应用合并部署降本增效

      +
      +
      +

      深圳市诺安赛威科技有限公司

      + +

      https://company.rfidworld.com.cn/Intro-137503.aspx

      +

      使用场景:动态热部署、热卸载、模块隔离

      +
      +
      +

      上海一嗨信息技术服务有限公司

      + +

      https://www.1hai.cn/

      +

      使用场景:多应用合并部署降本增效、模块热部署实现秒级构建发布

      +
      +
      +

      山东网聪信息科技有限公司

      + +

      https://www.gridnt.cn/

      +

      使用场景:类隔离

      +
      +
      +

      宋城独木桥网络有限公司

      + +

      https://www.dmqwl.com/

      +

      使用场景:动态热部署、热卸载、模块隔离

      +
      +
      +

      网商银行

      + +

      https://www.mybank.cn/

      +

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署

      +
      +
      +

      易宝支付有限公司

      + +

      https://www.yeepay.com/

      +

      使用场景:项目插件化,如动态部署、卸载、模块隔离

      +
      +
      +

      纵目科技(上海)股份有限公司

      + +

      https://www.zongmutech.com/

      +

      使用场景:多应用合并部署降本增效

      +
      + +
      + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + diff --git a/docs/public/user-cases/alibaba-aidc/index.html b/docs/public/user-cases/alibaba-aidc/index.html index 27d66e40f..9f0b72939 100644 --- a/docs/public/user-cases/alibaba-aidc/index.html +++ b/docs/public/user-cases/alibaba-aidc/index.html @@ -1,48 +1,541 @@ -阿里国际数字商业集团中台业务三倍提效 | SOFAServerless + + + + + + + + + + + + + + + + + + +阿里国际数字商业集团中台业务三倍提效 | SOFAServerless + + + + + + + + + + + + + + - - +基座:将一些业务应用迭代中不经常变更的基础组件以及 Lib 包下沉到一个新的应用中去,这个新应用我们称之为 基座应用,有以下特点: 基座是可独立发布与运维的 基座应用开发者可以统一升级中间件和组件版本,在保证兼容性的前提下,上层 App 无需感知整个升级过程 基座具备不同站点间的可复用性,一个交易的基座可以被 AE、Lazada、Daraz等不同的站点 App 共用 Serverless App:为了最大程度减少业务的改造成本,业务团队维护的 App 依旧保持其自身的发布运维职责的独立性不变,Serverless 化后业务开发人员只需要关心业务代码即可。JVM 对外服务的载体依旧是业务应用。 技术实施 Serverless 架构演技的实施过程分为两个部分:"/> + + + + + + + + + + + + + + + + + + + -

      阿里国际数字商业集团中台业务三倍提效

      作者: 朱林(封渊)、张建明(明门)

      项目背景

      过去几年,AIDC(阿里巴巴海外数字商业)业务板块在全球多个国家和地区拓展。国际电商业务模式上分为"跨境"和"本对本"两种,分别基于AE(跨境)、Lazada、Daraz、Mirivia、Trendyol 等多个电商平台承载。以下将不同的电商平台统称为“站点”。

      对于整个电商业务而言,各个站点核心的买卖家基础链路存在一定的差异性,但更多还是共性。通过抽象出一个通用的平台,在各个站点实现低成本复用有助于更高效的支持上层业务。所以过去几年基础链路一直在尝试通过平台化的建设思路,通过 1个全球化业务平台 + N个业务站点 的模式支持业务发展;其中技术的迭代经历了五个阶段的发展,从最初的中心化中台集成业务的架构模式,逐步转变为去中心化被业务集成的架构,已经基本可以满足全球化各个站点业务和平台各自闭环迭代。

      各个站点逻辑上是基于国际化中台(平台)做个性化定制,在交付/运维态各个站点被拆分成独立的应用,分别承载各自业务流量,平台能力通过二方包方式被站点应用集成,同时平台具备能力扩展机制,业务站点的研发能在站点应用中覆写平台逻辑,这最大化的保证了站点业务的研发/运维自主性,同时一定程度上保证了平台能力的复用性。 -但由于当前各个电商站点处于不同的发展时期,并且本对本跟跨境在业务模式上也存在差异性,以及业务策略上的不断变化,业务的快速迭代跟平台能力的后置沉淀逐步形成了矛盾,主要表现在如下几个方面:

      • 平台重复建设:由于平台采用开放、被集成的策略且缺乏一定的约束,需求的迭代即便是需要改动平台逻辑,站点也基本自闭环,在平台能力沉淀、稳定性、性能、开放性等各个站点都存在重复建设,支持不同站点的平台版本差异性逐步扩大;
      • 站点维护成本高:各自闭环的站点应用,由于同时维护了定制的平台能力,承担了部分"平台团队的职责",逐渐的增加了站点研发团队的负担,导致人力成本升高;
      • 研发迭代效率低:核心应用构建部署效率低下,以交易站点应用为例,系统启动时长稳定在 300s+,编译时长 150s+,镜像构建时长 30s+,容器重新初始化等调度层面的耗时 2min 左右,研发环境一天部署次数100+,如果能降低构建部署时长,将有效的降低研发等待时长;

      所以,下一代的架构迭代将需要重点解决平台去中心化被业务集成的架构模式下如何实现能力的迭代自主性以及版本统一,另外也需要考虑如何进一步降低站点的研发、运维成本,提升构建、部署效率,让业务研发真正只关注在自身的业务逻辑定制。 -Serverless 的技术理念中,强调关注点分离,让业务研发专注在业务逻辑的研发,而不太需要关注底层平台。这样一个理念,结合上述我们面临的问题或许是一个比较好的解法,让平台从二方包升级成为一个平台基座应用,统一收敛平台的迭代,包括应用运行时的升级;让业务站点应用轻量化,只关注定制逻辑的开发,提升部署效率,降低维护成本,整体逻辑架构图如下:

      概念阐述

      Serverless 普遍的理解是“无服务器架构”。它是云原生的主要技术之一,无服务器指的是用户不必关心应用运行和运维的管理,可以让用户去开发和使用应用程序,而不用去管理基础设施,云服务商提供、配置、管理用于运算的底层基础设施,将应用程序从基础结构中抽离出来,让开发人员可以更加专注于业务逻辑,从而提高交付能力,降低工作成本。传统 Serverless 在实现上其实就是 FaaS + BaaS。FaaS 承载代码片段(即函数),可随时随地创建、使用、销毁,无法自带状态。和 BaaS(后端即服务)搭配使用。两者合在一起,才最终实现了完整行为的 Serverless 服务。

      在传统 Serverless 技术体系下,Java 应用架构更多的是解决了 IaaS 层 + 容器化 的问题,Serverless 本身无法将涵盖范围下探到 JVM 内部。因此,对于一个复杂的 Java 巨石应用,可以借助 Serverless 的理念将 Java 技术栈下的业务代码和基础设施(中间件)依赖做更进一步的剥离与拆分。本次实践中的 Serverless 改造可以抽象为如下过程和目标:

      将一个单体应用横向拆分成上下两层:

      • 基座:将一些业务应用迭代中不经常变更的基础组件以及 Lib 包下沉到一个新的应用中去,这个新应用我们称之为 基座应用,有以下特点:
        • 基座是可独立发布与运维的
        • 基座应用开发者可以统一升级中间件和组件版本,在保证兼容性的前提下,上层 App 无需感知整个升级过程
        • 基座具备不同站点间的可复用性,一个交易的基座可以被 AE、Lazada、Daraz等不同的站点 App 共用
      • Serverless App:为了最大程度减少业务的改造成本,业务团队维护的 App 依旧保持其自身的发布运维职责的独立性不变,Serverless 化后业务开发人员只需要关心业务代码即可。JVM 对外服务的载体依旧是业务应用。

      技术实施

      Serverless 架构演技的实施过程分为两个部分:

      1. 重新设计 Serverless 架构模型下的应用架构分层与职责划分,让业务减负,让 SRE 提效。
      2. 在研发、发布&运维等领域采用新的研发框架、交付模型与发布运维产品来支持业务快速迭代。

      应用架构

      以 Daraz 基础链路为例,应用架构的分层结构、交互关系、团队职责如下:

      我们将一个 Serverless 应用完整交付所需的支撑架构进行逻辑分层与开发职责划分,并且清晰定义出 App 与基座之间交互的协议标准。

      研发域

      • 构建了 Serverless 运行时框架,驱动"基座-Serverless App"的运行与交互
      • 与 Aone 研发平台团队协作,建设一整套 Serverless 模式下基座与 App 的发布&运维产品体系

      voyager-serverless framework

      voyager-serverless framework 是一个基于 SOFAServerless 技术自研的提供 Serverless编程界面 的研发框架,允许业务 App 以 动态 的方式被装载到一个正在运行的基座容器(ArkContainer)中。在 SOFAServerless 模块隔离能力的基础上,我们针对阿里集团技术栈做了深度改造定制。

      整套框架提供以下关键能力:

      隔离性与运行时原理

      框架实现了基座与应用模块的 ClassLoader隔离SpringContext隔离。启动流程上,一共分为 两阶三步,启动顺序自底向上:

      • 一阶段基座启动
        • 第一步: 启动 Bootstrap,包含 Kondyle 以及 Pandora 容器,加载 Kondyle 插件 以及 Pandora 中间件插件类或对象
        • 第二步: 基座应用启动,其内部顺序为
          • 启动 ArkContainer,初始化 Serverless 相关组件
          • 基座应用 SpringContext 初始化,对象初始化过程中加载 基座自有类、基座Plugin类、依赖包类、中间件SDK类
      • 二阶段app启动
        • 第三步: Serverless App启动,由 ArkContainer 内置组件接受 Fiber 调度请求下载 App 制品并触发 App Launch
          • 创建 BizClassLoader,作为线程 ClassLoader 初始化 SpringContext,加载 App自有类、基座Plugin类、依赖包类、中间件SDK类

      通信机制

      在 Serverless 形态下,基座与 App 之间可以通过 进程内通信方式 进行交互,目前提供两种通信模型: SPI 基座Bean服务导出

      SPI 本质上就是基于标准 Java SPI 的扩展的内部特殊实现,本文不额外赘述,这里重点介绍下 基座Bean服务导出

      一般情况下,基座的 SpringContext 与 App 的 SpringContext 是完全隔离的,且没有父子继承关系。因此 App 侧不能通过常规 @Autowired 的方式引用下层基座 SpringContext 中的 Bean。 -除了 Class 的下沉,在一些场景下,基座可以将自己已经初始化好的 Bean 对象也下沉掉,声明并暴露给上层 App 使用。这样之后 App 启动的时候可以直接拿到基座 SpringContext 中的已经完成初始化的 Bean,用以加快 App 的启动速度。其过程如下:

      1. 用户通过配置或者注解标注声明需要在基座中导出的 Bean 服务
      2. 基座启动结束后,隔离容器会自动将用户标注的 Bean 导出到一个缓冲区中,等待 App 的启动使用
      3. App 在基座上启动,App 的 SpringContext 初始化过程中,会在初始化阶段读取到缓冲区中需要导入的 Bean
      4. App 的 SpringContext 中的其他组件可以正常 @Autowired 这些导出的 Bean

      插件机制

      Serverless 插件提供了一种能够让 App 运行时所需的类默认从基座中加载的机制,框架支持将平台基座需要暴露给上层 App 使用的SDK/二方包等包装成一个插件(Ark Plugin),最终实现中台控制的包下沉到基座而不需要让上层业务改动:

      中间件适配

      在 Serverless 架构演进中,由于一个完整应用的启动过程被拆分成基座启动和 App 启动,因此在一二阶段相关中间件的初始化逻辑也发生了变化。我们对国际侧常用的中间件和产品组件进行了测试,并对部分中间件进行了适配改造。 -总结起来,大部分问题都出现在中间件一些流程逻辑没设计这种多 ClassLoader 的场景,很多类/方法使用中不会将ClassLoader 对象作为参数传递进来,进而在初始化模型对象时出错,导致上下文交互异常。

      开发配套

      我们也提供了一整套完整且简单易用的配套工具,方便开发者快速接入 Serverless 体系:

      发布 & 运维域

      除了研发域,Serverless 架构在发布运维领域也带来很多新的变化。首先是研发运维分层拆分,实现了关注点分离,降低研发复杂度:

      • 逻辑拆分:将原本应用拆分,将业务代码和基础设施隔离,基础能力下沉,比如将改造前启动过程中耗时中间件、一些富二方库和需要管控的二方库等等下沉到基座中,实现了业务应用轻量化。
      • 独立演进:分层拆分后,基座和业务应用各自迭代,SRE 可以在基座上将基础设施的统一管控和升级,较少甚至杜绝了业务的升级成本。

      我们也和 Aone 一起合作,voyager-serverless 借助 OSR(Open Serverless Runtime) 标准协议接入 Aone Serverless 产品技术体系中去。借助新的发布模型和部署策略,在 App 构建速度和启动效率上得到很大提升。

      构建效率提升

      • Maven 构建优化: 由于很多依赖包都已经下沉到已经就绪的基座,因此对于 App 来说就可以减少需要构建的二方包数量以及 Class 文件数量,进而整体优化制品大小,提升构建效率
      • Docker 构建移除: 由于 Serverless 模式下业务 App 部署的制品就是轻量化 Fat Jar,因此也无需进行 docker 构建

      发布效率提升

      在 Serverless 模式下,我们采用 Surge+流式发布 替换传统的分批发布来进一步提升 App 的发布效率。

      名词描述
      分批发布   分批发布策略是达到每个批次的新节点后,进行下一批次,如 100 个节点,10 个批次,第一批次卡点 10 个新节点,第二批次卡点 20 个新节点,依次类推
      Surge   Surge 发布策略可以在不影响业务可用性的前提下加速业务发布效率:
      1) 发布时会新增加配置 Surge 比例的数量节点,比如业务有 10 台机器,Surge 配置百分比为 50,发布过程就会首先增加 5 台机器用于发布
      2) 如果基座中配置了合理大小的 Buffer,那么可以直接从 Buffer 中获取这 5 台机器,直接发布新版本代码
      3) 整体新版本节点数达到预期数量(本例中 10 台机器),直接下线旧节点,完成整次发布过程
      其中 Surge 结合流式发布一起使用,同时配合 Runtime 中合适数量的 Buffer,可以最大程度地加速业务发布效率
      • 瀑布流分批发布:每一个批次的机器全部发布上线之后开始发布下一批次,批次内机器并行发布,批次之间串行。假设有100台机器,分10批次发布,每批发布的机器数为10台,发布总耗时为:

      • Surge流式发布:发布过程中,允许多分配一些机器来参与更新,核心原理为 满足可用度的前提 下,增大单轮次中参与更新的机器个数。例如有100台机器,在满足可用度≥90%的前提下,即任意时刻至少有90(100*90%)个机器在线,surge=5%的发布调度过程如下:

      借助这个新的发布模型,我们在开发变更最频繁的日常和预发环境全面开启 Surge 发布,用来加速业务 App 的发布。

      • 在进行 Serverless 改造前:
        • 为了保证发布过程中流量不受影响,一般情况下,一个预发环境会保留两台机器(replica = 2),执行传统的分批发布(batch=2),也就是每台机器轮流更新。
        • 这里我们假定应用启动耗时为5min,其中频繁变更的业务代码1min,平台以及中间件等组件加载耗时4min
        • 发布总耗时为 5min + 5min = 10min

      • 完成 Serverless 改造,采用 Surge 流式发布后
        • App 的预发环境只需要保留一台机器(replica = 1),基座设置buffer = 1,即保留一台空基座用于准备给 App 调度使用
        • 发布策略上,App 环境 Surge 比例为100%
        • 由于只发布更新了 App 的 Biz 代码,发布总耗时为 1min,并且整个过程机器总成本保持不变

      同时,我们也在生产环境配置一定数量的基座 Buffer,支持站点 App 的快速弹性扩容。

      总结与展望

      目前已经完成 Daraz 业务站点的交易、营销、履约、会员、逆向应用的 Serverless 升级改造,在 构建时长单机应用启动时长预发环境发布时长 三个指标上均取得了较大优化效果。在个别领域,甚至真正做到了 App 的 10秒 级启动。

      可以看到,本次 Serverless 架构的升级,无论从理论推演还是实践结果,都产生了较大的正向收益与效率提升,这给后续业务 App 的快速迭代带来了不少便利。同时,由于平台代码下沉为基座应用,也具备了跟业务站点正交的发布能力,基本上可以实现基础链路平台版本统一的目标;“关注点分离"也解放了业务开发者,让他们更多关注在自己的业务代码上。但是,还有一些如开发配套成熟度、问题定位与诊断、生产环境成本最优的基座配置等问题与挑战需要进一步解决。我们也会深度参与共建 SOFAServerless 开源社区,发掘更多的落地场景与实践经验。

      Serverless 从来不是某个单一的架构形态,它带来的更多是一种理念和生产方式。理解它、利用它,帮助我们拓宽新的思路与解题方法。

      -

      最后修改 October 30, 2023: update home (f751b32b)
      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      + + +
      + + + + + +
      +

      阿里国际数字商业集团中台业务三倍提效

      + + +
      +

      作者: 朱林(封渊)、张建明(明门)

      +
      +

      项目背景

      +

      过去几年,AIDC(阿里巴巴海外数字商业)业务板块在全球多个国家和地区拓展。国际电商业务模式上分为"跨境"和"本对本"两种,分别基于AE(跨境)、Lazada、Daraz、Mirivia、Trendyol 等多个电商平台承载。以下将不同的电商平台统称为“站点”。

      +

      +

      对于整个电商业务而言,各个站点核心的买卖家基础链路存在一定的差异性,但更多还是共性。通过抽象出一个通用的平台,在各个站点实现低成本复用有助于更高效的支持上层业务。所以过去几年基础链路一直在尝试通过平台化的建设思路,通过 1个全球化业务平台 + N个业务站点 的模式支持业务发展;其中技术的迭代经历了五个阶段的发展,从最初的中心化中台集成业务的架构模式,逐步转变为去中心化被业务集成的架构,已经基本可以满足全球化各个站点业务和平台各自闭环迭代。

      +

      +

      各个站点逻辑上是基于国际化中台(平台)做个性化定制,在交付/运维态各个站点被拆分成独立的应用,分别承载各自业务流量,平台能力通过二方包方式被站点应用集成,同时平台具备能力扩展机制,业务站点的研发能在站点应用中覆写平台逻辑,这最大化的保证了站点业务的研发/运维自主性,同时一定程度上保证了平台能力的复用性。 +但由于当前各个电商站点处于不同的发展时期,并且本对本跟跨境在业务模式上也存在差异性,以及业务策略上的不断变化,业务的快速迭代跟平台能力的后置沉淀逐步形成了矛盾,主要表现在如下几个方面:

      +
        +
      • 平台重复建设:由于平台采用开放、被集成的策略且缺乏一定的约束,需求的迭代即便是需要改动平台逻辑,站点也基本自闭环,在平台能力沉淀、稳定性、性能、开放性等各个站点都存在重复建设,支持不同站点的平台版本差异性逐步扩大;
      • +
      • 站点维护成本高:各自闭环的站点应用,由于同时维护了定制的平台能力,承担了部分"平台团队的职责",逐渐的增加了站点研发团队的负担,导致人力成本升高;
      • +
      • 研发迭代效率低:核心应用构建部署效率低下,以交易站点应用为例,系统启动时长稳定在 300s+,编译时长 150s+,镜像构建时长 30s+,容器重新初始化等调度层面的耗时 2min 左右,研发环境一天部署次数100+,如果能降低构建部署时长,将有效的降低研发等待时长;
      • +
      +

      所以,下一代的架构迭代将需要重点解决平台去中心化被业务集成的架构模式下如何实现能力的迭代自主性以及版本统一,另外也需要考虑如何进一步降低站点的研发、运维成本,提升构建、部署效率,让业务研发真正只关注在自身的业务逻辑定制。 +Serverless 的技术理念中,强调关注点分离,让业务研发专注在业务逻辑的研发,而不太需要关注底层平台。这样一个理念,结合上述我们面临的问题或许是一个比较好的解法,让平台从二方包升级成为一个平台基座应用,统一收敛平台的迭代,包括应用运行时的升级;让业务站点应用轻量化,只关注定制逻辑的开发,提升部署效率,降低维护成本,整体逻辑架构图如下:

      +

      +

      概念阐述

      +

      Serverless 普遍的理解是“无服务器架构”。它是云原生的主要技术之一,无服务器指的是用户不必关心应用运行和运维的管理,可以让用户去开发和使用应用程序,而不用去管理基础设施,云服务商提供、配置、管理用于运算的底层基础设施,将应用程序从基础结构中抽离出来,让开发人员可以更加专注于业务逻辑,从而提高交付能力,降低工作成本。传统 Serverless 在实现上其实就是 FaaS + BaaS。FaaS 承载代码片段(即函数),可随时随地创建、使用、销毁,无法自带状态。和 BaaS(后端即服务)搭配使用。两者合在一起,才最终实现了完整行为的 Serverless 服务。

      +

      +

      在传统 Serverless 技术体系下,Java 应用架构更多的是解决了 IaaS 层 + 容器化 的问题,Serverless 本身无法将涵盖范围下探到 JVM 内部。因此,对于一个复杂的 Java 巨石应用,可以借助 Serverless 的理念将 Java 技术栈下的业务代码和基础设施(中间件)依赖做更进一步的剥离与拆分。本次实践中的 Serverless 改造可以抽象为如下过程和目标:

      +

      +

      将一个单体应用横向拆分成上下两层:

      +
        +
      • 基座:将一些业务应用迭代中不经常变更的基础组件以及 Lib 包下沉到一个新的应用中去,这个新应用我们称之为 基座应用,有以下特点: +
          +
        • 基座是可独立发布与运维的
        • +
        • 基座应用开发者可以统一升级中间件和组件版本,在保证兼容性的前提下,上层 App 无需感知整个升级过程
        • +
        • 基座具备不同站点间的可复用性,一个交易的基座可以被 AE、Lazada、Daraz等不同的站点 App 共用
        • +
        +
      • +
      • Serverless App:为了最大程度减少业务的改造成本,业务团队维护的 App 依旧保持其自身的发布运维职责的独立性不变,Serverless 化后业务开发人员只需要关心业务代码即可。JVM 对外服务的载体依旧是业务应用。
      • +
      +

      技术实施

      +

      +

      Serverless 架构演技的实施过程分为两个部分:

      +
        +
      1. 重新设计 Serverless 架构模型下的应用架构分层与职责划分,让业务减负,让 SRE 提效。
      2. +
      3. 在研发、发布&运维等领域采用新的研发框架、交付模型与发布运维产品来支持业务快速迭代。
      4. +
      +

      应用架构

      +

      以 Daraz 基础链路为例,应用架构的分层结构、交互关系、团队职责如下:

      +

      +

      我们将一个 Serverless 应用完整交付所需的支撑架构进行逻辑分层与开发职责划分,并且清晰定义出 App 与基座之间交互的协议标准。

      +

      研发域

      +

      +
        +
      • 构建了 Serverless 运行时框架,驱动"基座-Serverless App"的运行与交互
      • +
      • 与 Aone 研发平台团队协作,建设一整套 Serverless 模式下基座与 App 的发布&运维产品体系
      • +
      +

      voyager-serverless framework

      +

      +

      voyager-serverless framework 是一个基于 SOFAServerless 技术自研的提供 Serverless编程界面 的研发框架,允许业务 App 以 动态 的方式被装载到一个正在运行的基座容器(ArkContainer)中。在 SOFAServerless 模块隔离能力的基础上,我们针对阿里集团技术栈做了深度改造定制。

      +

      整套框架提供以下关键能力:

      +

      +

      隔离性与运行时原理

      +

      +

      框架实现了基座与应用模块的 ClassLoader隔离SpringContext隔离。启动流程上,一共分为 两阶三步,启动顺序自底向上:

      +
        +
      • 一阶段基座启动 +
          +
        • 第一步: 启动 Bootstrap,包含 Kondyle 以及 Pandora 容器,加载 Kondyle 插件 以及 Pandora 中间件插件类或对象
        • +
        • 第二步: 基座应用启动,其内部顺序为 +
            +
          • 启动 ArkContainer,初始化 Serverless 相关组件
          • +
          • 基座应用 SpringContext 初始化,对象初始化过程中加载 基座自有类、基座Plugin类、依赖包类、中间件SDK类
          • +
          +
        • +
        +
      • +
      • 二阶段app启动 +
          +
        • 第三步: Serverless App启动,由 ArkContainer 内置组件接受 Fiber 调度请求下载 App 制品并触发 App Launch +
            +
          • 创建 BizClassLoader,作为线程 ClassLoader 初始化 SpringContext,加载 App自有类、基座Plugin类、依赖包类、中间件SDK类
          • +
          +
        • +
        +
      • +
      +

      通信机制

      +

      在 Serverless 形态下,基座与 App 之间可以通过 进程内通信方式 进行交互,目前提供两种通信模型: SPI 基座Bean服务导出

      +
      +

      SPI 本质上就是基于标准 Java SPI 的扩展的内部特殊实现,本文不额外赘述,这里重点介绍下 基座Bean服务导出

      +
      +

      一般情况下,基座的 SpringContext 与 App 的 SpringContext 是完全隔离的,且没有父子继承关系。因此 App 侧不能通过常规 @Autowired 的方式引用下层基座 SpringContext 中的 Bean。 +除了 Class 的下沉,在一些场景下,基座可以将自己已经初始化好的 Bean 对象也下沉掉,声明并暴露给上层 App 使用。这样之后 App 启动的时候可以直接拿到基座 SpringContext 中的已经完成初始化的 Bean,用以加快 App 的启动速度。其过程如下:

      +

      +
        +
      1. 用户通过配置或者注解标注声明需要在基座中导出的 Bean 服务
      2. +
      3. 基座启动结束后,隔离容器会自动将用户标注的 Bean 导出到一个缓冲区中,等待 App 的启动使用
      4. +
      5. App 在基座上启动,App 的 SpringContext 初始化过程中,会在初始化阶段读取到缓冲区中需要导入的 Bean
      6. +
      7. App 的 SpringContext 中的其他组件可以正常 @Autowired 这些导出的 Bean
      8. +
      +

      插件机制

      +

      Serverless 插件提供了一种能够让 App 运行时所需的类默认从基座中加载的机制,框架支持将平台基座需要暴露给上层 App 使用的SDK/二方包等包装成一个插件(Ark Plugin),最终实现中台控制的包下沉到基座而不需要让上层业务改动:

      +

      +

      中间件适配

      +

      在 Serverless 架构演进中,由于一个完整应用的启动过程被拆分成基座启动和 App 启动,因此在一二阶段相关中间件的初始化逻辑也发生了变化。我们对国际侧常用的中间件和产品组件进行了测试,并对部分中间件进行了适配改造。 +总结起来,大部分问题都出现在中间件一些流程逻辑没设计这种多 ClassLoader 的场景,很多类/方法使用中不会将ClassLoader 对象作为参数传递进来,进而在初始化模型对象时出错,导致上下文交互异常。

      +

      开发配套

      +

      我们也提供了一整套完整且简单易用的配套工具,方便开发者快速接入 Serverless 体系:

      +

      +

      发布 & 运维域

      +

      除了研发域,Serverless 架构在发布运维领域也带来很多新的变化。首先是研发运维分层拆分,实现了关注点分离,降低研发复杂度:

      +

      +
        +
      • 逻辑拆分:将原本应用拆分,将业务代码和基础设施隔离,基础能力下沉,比如将改造前启动过程中耗时中间件、一些富二方库和需要管控的二方库等等下沉到基座中,实现了业务应用轻量化。
      • +
      • 独立演进:分层拆分后,基座和业务应用各自迭代,SRE 可以在基座上将基础设施的统一管控和升级,较少甚至杜绝了业务的升级成本。
      • +
      +

      +

      我们也和 Aone 一起合作,voyager-serverless 借助 OSR(Open Serverless Runtime) 标准协议接入 Aone Serverless 产品技术体系中去。借助新的发布模型和部署策略,在 App 构建速度和启动效率上得到很大提升。

      +

      +

      构建效率提升

      +
        +
      • Maven 构建优化: 由于很多依赖包都已经下沉到已经就绪的基座,因此对于 App 来说就可以减少需要构建的二方包数量以及 Class 文件数量,进而整体优化制品大小,提升构建效率
      • +
      • Docker 构建移除: 由于 Serverless 模式下业务 App 部署的制品就是轻量化 Fat Jar,因此也无需进行 docker 构建
      • +
      +

      发布效率提升

      +

      在 Serverless 模式下,我们采用 Surge+流式发布 替换传统的分批发布来进一步提升 App 的发布效率。

      + + + + + + + + + + + + + + + + + +
      名词描述
      分批发布   分批发布策略是达到每个批次的新节点后,进行下一批次,如 100 个节点,10 个批次,第一批次卡点 10 个新节点,第二批次卡点 20 个新节点,依次类推
      Surge   Surge 发布策略可以在不影响业务可用性的前提下加速业务发布效率:
      1) 发布时会新增加配置 Surge 比例的数量节点,比如业务有 10 台机器,Surge 配置百分比为 50,发布过程就会首先增加 5 台机器用于发布
      2) 如果基座中配置了合理大小的 Buffer,那么可以直接从 Buffer 中获取这 5 台机器,直接发布新版本代码
      3) 整体新版本节点数达到预期数量(本例中 10 台机器),直接下线旧节点,完成整次发布过程
      其中 Surge 结合流式发布一起使用,同时配合 Runtime 中合适数量的 Buffer,可以最大程度地加速业务发布效率
      +
        +
      • 瀑布流分批发布:每一个批次的机器全部发布上线之后开始发布下一批次,批次内机器并行发布,批次之间串行。假设有100台机器,分10批次发布,每批发布的机器数为10台,发布总耗时为:
      • +
      +

      +
        +
      • Surge流式发布:发布过程中,允许多分配一些机器来参与更新,核心原理为 满足可用度的前提 下,增大单轮次中参与更新的机器个数。例如有100台机器,在满足可用度≥90%的前提下,即任意时刻至少有90(100*90%)个机器在线,surge=5%的发布调度过程如下:
      • +
      +

      +

      +

      借助这个新的发布模型,我们在开发变更最频繁的日常和预发环境全面开启 Surge 发布,用来加速业务 App 的发布。

      +
        +
      • 在进行 Serverless 改造前: +
          +
        • 为了保证发布过程中流量不受影响,一般情况下,一个预发环境会保留两台机器(replica = 2),执行传统的分批发布(batch=2),也就是每台机器轮流更新。
        • +
        • 这里我们假定应用启动耗时为5min,其中频繁变更的业务代码1min,平台以及中间件等组件加载耗时4min
        • +
        • 发布总耗时为 5min + 5min = 10min
        • +
        +
      • +
      +

      +
        +
      • 完成 Serverless 改造,采用 Surge 流式发布后 +
          +
        • App 的预发环境只需要保留一台机器(replica = 1),基座设置buffer = 1,即保留一台空基座用于准备给 App 调度使用
        • +
        • 发布策略上,App 环境 Surge 比例为100%
        • +
        • 由于只发布更新了 App 的 Biz 代码,发布总耗时为 1min,并且整个过程机器总成本保持不变
        • +
        +
      • +
      +

      +

      同时,我们也在生产环境配置一定数量的基座 Buffer,支持站点 App 的快速弹性扩容。

      +

      总结与展望

      +

      目前已经完成 Daraz 业务站点的交易、营销、履约、会员、逆向应用的 Serverless 升级改造,在 构建时长单机应用启动时长预发环境发布时长 三个指标上均取得了较大优化效果。在个别领域,甚至真正做到了 App 的 10秒 级启动。

      +

      +

      可以看到,本次 Serverless 架构的升级,无论从理论推演还是实践结果,都产生了较大的正向收益与效率提升,这给后续业务 App 的快速迭代带来了不少便利。同时,由于平台代码下沉为基座应用,也具备了跟业务站点正交的发布能力,基本上可以实现基础链路平台版本统一的目标;“关注点分离"也解放了业务开发者,让他们更多关注在自己的业务代码上。但是,还有一些如开发配套成熟度、问题定位与诊断、生产环境成本最优的基座配置等问题与挑战需要进一步解决。我们也会深度参与共建 SOFAServerless 开源社区,发掘更多的落地场景与实践经验。

      +

      Serverless 从来不是某个单一的架构形态,它带来的更多是一种理念和生产方式。理解它、利用它,帮助我们拓宽新的思路与解题方法。

      + + +
      + + + + + + +
      + + +
      +
      + 最后修改 October 30, 2023: update home (f751b32b) +
      + +
      + + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/user-cases/all-users/index.html b/docs/public/user-cases/all-users/index.html index acf066e4d..542136f3d 100644 --- a/docs/public/user-cases/all-users/index.html +++ b/docs/public/user-cases/all-users/index.html @@ -1,4 +1,25 @@ -SOFAServerless 所有企业案例 | SOFAServerless + + + + + + + + + + + + + + + + + + +SOFAServerless 所有企业案例 | SOFAServerless + + + + + + + + + + + + + + - - +使用场景:多应用合并部署降本增效"/> + + + + + + + + + + + + + + + + + + + -

      SOFAServerless 所有企业案例

      目前主动登记使用 SOFAServerless 的企业,按企业拼音字母顺序排序,不分先后:


      阿里国际数字商业集团

      使用场景:通用基座实现普通应用秒级构建发布、SDK 无感升级。详细案例



      阿里健康科技(中国)有限公司

      https://www.alihealth.cn/

      使用场景:应用插件化开发



      阿里妈妈

      https://www.alihealth.cn/

      使用场景:合并部署、热部署



      斑马信息科技有限公司

      https://www.ebanma.com/

      使用场景:类隔离、合并部署



      成都云智天下科技股份有限公司

      https://www.yunzhitx.com/

      使用场景:模块动态部署、卸载



      华信永道(北京)科技股份有限公司

      使用场景:多应用合并部署降本增效

      https://www.yondervision.com.cn/



      蚂蚁集团

      https://www.antgroup.com/

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署,实现资源降本和研发提效。详细案例



      南京爱福路汽车科技有限公司

      https://www.f6car.cn/

      使用场景:多应用合并部署降本增效



      深圳市诺安赛威科技有限公司

      https://company.rfidworld.com.cn/Intro-137503.aspx

      使用场景:动态热部署、热卸载、模块隔离



      上海一嗨信息技术服务有限公司

      https://www.1hai.cn/

      使用场景:多应用合并部署降本增效、模块热部署实现秒级构建发布



      山东网聪信息科技有限公司

      https://www.gridnt.cn/

      使用场景:类隔离



      宋城独木桥网络有限公司

      https://www.dmqwl.com/

      使用场景:动态热部署、热卸载、模块隔离



      网商银行

      https://www.mybank.cn/

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署



      易宝支付有限公司

      https://www.yeepay.com/

      使用场景:项目插件化,如动态部署、卸载、模块隔离



      纵目科技(上海)股份有限公司

      https://www.zongmutech.com/

      使用场景:多应用合并部署降本增效


      -

      最后修改 November 21, 2023: update docs (a0a94aa6)
      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      + + +
      + + + + + +
      +

      SOFAServerless 所有企业案例

      + + +

      目前主动登记使用 SOFAServerless 的企业,按企业拼音字母顺序排序,不分先后:

      +
      +

      阿里国际数字商业集团

      + +

      使用场景:通用基座实现普通应用秒级构建发布、SDK 无感升级。详细案例

      +
      +
      +

      阿里健康科技(中国)有限公司

      + +

      https://www.alihealth.cn/

      +

      使用场景:应用插件化开发

      +
      +
      +

      阿里妈妈

      + +

      https://www.alihealth.cn/

      +

      使用场景:合并部署、热部署

      +
      +
      +

      斑马信息科技有限公司

      + +

      https://www.ebanma.com/

      +

      使用场景:类隔离、合并部署

      +
      +
      +

      成都云智天下科技股份有限公司

      + +

      https://www.yunzhitx.com/

      +

      使用场景:模块动态部署、卸载

      +
      +
      +

      华信永道(北京)科技股份有限公司

      +

      使用场景:多应用合并部署降本增效

      + +

      https://www.yondervision.com.cn/

      +
      +
      +

      蚂蚁集团

      + +

      https://www.antgroup.com/

      +

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署,实现资源降本和研发提效。详细案例

      +
      +
      +

      南京爱福路汽车科技有限公司

      + +

      https://www.f6car.cn/

      +

      使用场景:多应用合并部署降本增效

      +
      +
      +

      深圳市诺安赛威科技有限公司

      + +

      https://company.rfidworld.com.cn/Intro-137503.aspx

      +

      使用场景:动态热部署、热卸载、模块隔离

      +
      +
      +

      上海一嗨信息技术服务有限公司

      + +

      https://www.1hai.cn/

      +

      使用场景:多应用合并部署降本增效、模块热部署实现秒级构建发布

      +
      +
      +

      山东网聪信息科技有限公司

      + +

      https://www.gridnt.cn/

      +

      使用场景:类隔离

      +
      +
      +

      宋城独木桥网络有限公司

      + +

      https://www.dmqwl.com/

      +

      使用场景:动态热部署、热卸载、模块隔离

      +
      +
      +

      网商银行

      + +

      https://www.mybank.cn/

      +

      使用场景:应用秒级构建发布、秒级弹性、并行迭代、合并部署

      +
      +
      +

      易宝支付有限公司

      + +

      https://www.yeepay.com/

      +

      使用场景:项目插件化,如动态部署、卸载、模块隔离

      +
      +
      +

      纵目科技(上海)股份有限公司

      + +

      https://www.zongmutech.com/

      +

      使用场景:多应用合并部署降本增效

      +
      + + +
      + + + + + + +
      + + +
      +
      + 最后修改 November 21, 2023: update docs (a0a94aa6) +
      + +
      + + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/user-cases/ant-group/index.html b/docs/public/user-cases/ant-group/index.html index b3d54f3aa..4d277ead4 100644 --- a/docs/public/user-cases/ant-group/index.html +++ b/docs/public/user-cases/ant-group/index.html @@ -1,6 +1,27 @@ -蚂蚁集团大规模 Serverless 降本增效实践 | SOFAServerless + + + + + + + + + + + + + + + + + + +蚂蚁集团大规模 Serverless 降本增效实践 | SOFAServerless + + + + + + + + + + + + + + - - +在 SOFAServerless 通用基座模式里,基座会提前启动好,这些预启动的基座包含了各种通用中间件、二方包和三方包的 SDK。借助 SOFAServerless 构建插件,业务应用会被构建成 FatJar 包,在发布业务应用新版本时,调度器会选择一台没有安装模块的空基座将模块应用 FatJar 热部署到基座,装有模块的老基座服务器会异步的替换成新服务器(空基座)。"/> + + + + + + + + + + + + + + + + + + + -

      蚂蚁集团大规模 Serverless 降本增效实践

      作者:刘煜、赵真灵、刘晶、代巍、孙仁恩等

      蚂蚁集团业务痛点

      蚂蚁集团过去 20 年经历了微服务架构飞速演进,与此同时应用数量和复杂度爆发式增长,带来了严重的企业成本和效率问题:

      1. 大量长尾应用 CPU 使用率不足 10%,却由于多地域高可用消耗大量机器。
      2. 应用一次构建+发布速度慢,平均 10 分钟,研发效率低下且无法快速弹性
      3. 多人开发的应用,功能必须攒一起 “赶火车”,迭代互相阻塞,协作和交付效率低下。
      4. 业务 SDK 和部分框架升级对业务有较高打扰,无法做到基础设施对业务微感甚至无感。
      5. 业务资产难以沉淀,大中台建设成本高昂

      蚂蚁集团 SOFAServerless 使用场景

      合并部署降成本

      在企业中 “80%” 的长尾应用仅服务 “20%” 的流量,蚂蚁集团也不例外。
      在蚂蚁集团存在大量长尾应用,每个长尾应用至少需要 预发布、灰度、生产 3 个环境,每个环境最少需要部署 3 个机房,每个机房又必须保持 2 台机器高可用,因此大量长尾应用 CPU 使用率不足 10%
      通过使用 SOFAServerless,蚂蚁集团对长尾应用进行了服务器裁撤,借助类委托隔离、资源监控、日志监控等技术,在保证稳定性的前提下,实现了多应用的合并部署,极大降低了业务的运维成本和资源成本。

      此外,采用这种模式,小应用可以不走应用申请上线流程也不需要申请机器,可以直接部署到业务通用基座之上,从而帮助小流量业务实现了快速创新。

      模块化研发极致提效

      在蚂蚁集团,很多部门存在开发者人数较多的应用,由于人数多,导致环境抢占、联调抢占、测试资源抢占情况严重,互相阻塞,一人 Delay 多人 Delay,导致需求交付效率低下。
      通过使用 SOFAServerless,蚂蚁集团将协作人数较多的应用,一步步重构为基座代码和不同功能的模块代码。基座代码沉淀了各种 SDK 和业务的公共接口,由专人维护,而模块代码则内聚了某一个功能领域特有的业务逻辑,可以调用本地基座接口。模块采用热部署实现了十秒级构建、发布、伸缩,同时模块开发者完全不用关心服务器和基础设施,这样普通应用便以很低的接入成本实现了 Serverless 的研发体验。
      以蚂蚁集团资金业务为例,资金通过将应用拆分为一个基座与多个模块,实现了发布运维、组织协作、集群流量隔离多个维度的极致提效。

      蚂蚁集团资金业务 SOFAServerless 架构演进和实践,详见:https://mp.weixin.qq.com/s/uN0SyzkW_elYIwi03gis-Q

      通用基座屏蔽基础设施

      在蚂蚁集团,各种 SDK 的升级打扰、构建发布慢是痛点问题。借助 SOFAServerless 通用基座模式,蚂蚁集团帮助部分应用实现了基础设施微感升级,同时应用的构建与发布速度也从 600 秒减少到了 90 秒

      在 SOFAServerless 通用基座模式里,基座会提前启动好,这些预启动的基座包含了各种通用中间件、二方包和三方包的 SDK。借助 SOFAServerless 构建插件,业务应用会被构建成 FatJar 包,在发布业务应用新版本时,调度器会选择一台没有安装模块的空基座将模块应用 FatJar 热部署到基座,装有模块的老基座服务器会异步的替换成新服务器(空基座)。
      基座由专职团队负责维护和升级,对模块应用开发者来说,便享受到了基础设施无感升级和极速构建发布体验。

      低成本实现高效中台

      在蚂蚁集团,有不少中台类业务,典型如各个业务线的玩法、营销、公益、搜索推荐、广告投放等。通过使用 SOFAServerless,这些中台业务逐渐演进成了基座 + 模块的交付方式,其中基座代码沉淀了通用逻辑,也定义了一些 SPI,而模块负责实现这些 SPI,流量会从基座代码进入,调用模块的 SPI 实现。
      在中台场景下,模块一般都很轻,甚至只是一个代码片段,大部分模块都能在 5 秒内发布、扩容完成,而且模块开发者完全不关心基础设施,享受到了极致的 Serverless 研发体验。
      以蚂蚁集团搜索推荐业务中台为例,搜索推荐业务将公共依赖、通用逻辑、流程引擎全部下沉到基座,并且定义了一些 SPI,搜索推荐算法由各个模块开发者实现,当前搜索推荐已经接入了 1000+ 模块,平均代码发布上线不到 1 天,真正实现了代码的 “朝写夕发”。

      总结与规划

      SOFAServerless 在蚂蚁集团历经 3 年多的演进与打磨,目前已在所有业务线完成落地,支撑了全集团 1/4 的在线流量,帮助全集团实现了功能平均上线从 12 天减少到 6 天、长尾应用服务器砍半秒级发布运维的极致降本增效结果。
      未来,蚂蚁集团会更大规模推广 SOFAServerless 研发模式,并持续建设弹性能力,做到更极致的弹性体验和绿色低碳。同时,我们也会重点投入开源能力建设,希望和社区同学共同打造极致的模块化技术体系,为各行各业的软件创造技术价值,助力企业实现降本提效。

      -

      最后修改 October 30, 2023: update home (f751b32b)
      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      + + +
      + + + + + +
      +

      蚂蚁集团大规模 Serverless 降本增效实践

      + + +
      +

      作者:刘煜、赵真灵、刘晶、代巍、孙仁恩等

      +
      +

      蚂蚁集团业务痛点

      +

      蚂蚁集团过去 20 年经历了微服务架构飞速演进,与此同时应用数量和复杂度爆发式增长,带来了严重的企业成本和效率问题:

      +
        +
      1. 大量长尾应用 CPU 使用率不足 10%,却由于多地域高可用消耗大量机器。
      2. +
      3. 应用一次构建+发布速度慢,平均 10 分钟,研发效率低下且无法快速弹性
      4. +
      5. 多人开发的应用,功能必须攒一起 “赶火车”,迭代互相阻塞,协作和交付效率低下。
      6. +
      7. 业务 SDK 和部分框架升级对业务有较高打扰,无法做到基础设施对业务微感甚至无感。
      8. +
      9. 业务资产难以沉淀,大中台建设成本高昂
      10. +
      +

      蚂蚁集团 SOFAServerless 使用场景

      +

      合并部署降成本

      +

      在企业中 “80%” 的长尾应用仅服务 “20%” 的流量,蚂蚁集团也不例外。
      在蚂蚁集团存在大量长尾应用,每个长尾应用至少需要 预发布、灰度、生产 3 个环境,每个环境最少需要部署 3 个机房,每个机房又必须保持 2 台机器高可用,因此大量长尾应用 CPU 使用率不足 10%
      通过使用 SOFAServerless,蚂蚁集团对长尾应用进行了服务器裁撤,借助类委托隔离、资源监控、日志监控等技术,在保证稳定性的前提下,实现了多应用的合并部署,极大降低了业务的运维成本和资源成本。

      此外,采用这种模式,小应用可以不走应用申请上线流程也不需要申请机器,可以直接部署到业务通用基座之上,从而帮助小流量业务实现了快速创新。

      +

      模块化研发极致提效

      +

      在蚂蚁集团,很多部门存在开发者人数较多的应用,由于人数多,导致环境抢占、联调抢占、测试资源抢占情况严重,互相阻塞,一人 Delay 多人 Delay,导致需求交付效率低下。
      通过使用 SOFAServerless,蚂蚁集团将协作人数较多的应用,一步步重构为基座代码和不同功能的模块代码。基座代码沉淀了各种 SDK 和业务的公共接口,由专人维护,而模块代码则内聚了某一个功能领域特有的业务逻辑,可以调用本地基座接口。模块采用热部署实现了十秒级构建、发布、伸缩,同时模块开发者完全不用关心服务器和基础设施,这样普通应用便以很低的接入成本实现了 Serverless 的研发体验。
      以蚂蚁集团资金业务为例,资金通过将应用拆分为一个基座与多个模块,实现了发布运维、组织协作、集群流量隔离多个维度的极致提效。

      +

      蚂蚁集团资金业务 SOFAServerless 架构演进和实践,详见:https://mp.weixin.qq.com/s/uN0SyzkW_elYIwi03gis-Q

      +

      通用基座屏蔽基础设施

      +

      在蚂蚁集团,各种 SDK 的升级打扰、构建发布慢是痛点问题。借助 SOFAServerless 通用基座模式,蚂蚁集团帮助部分应用实现了基础设施微感升级,同时应用的构建与发布速度也从 600 秒减少到了 90 秒

      +

      在 SOFAServerless 通用基座模式里,基座会提前启动好,这些预启动的基座包含了各种通用中间件、二方包和三方包的 SDK。借助 SOFAServerless 构建插件,业务应用会被构建成 FatJar 包,在发布业务应用新版本时,调度器会选择一台没有安装模块的空基座将模块应用 FatJar 热部署到基座,装有模块的老基座服务器会异步的替换成新服务器(空基座)。
      基座由专职团队负责维护和升级,对模块应用开发者来说,便享受到了基础设施无感升级和极速构建发布体验。

      +

      低成本实现高效中台

      +

      在蚂蚁集团,有不少中台类业务,典型如各个业务线的玩法、营销、公益、搜索推荐、广告投放等。通过使用 SOFAServerless,这些中台业务逐渐演进成了基座 + 模块的交付方式,其中基座代码沉淀了通用逻辑,也定义了一些 SPI,而模块负责实现这些 SPI,流量会从基座代码进入,调用模块的 SPI 实现。
      在中台场景下,模块一般都很轻,甚至只是一个代码片段,大部分模块都能在 5 秒内发布、扩容完成,而且模块开发者完全不关心基础设施,享受到了极致的 Serverless 研发体验。
      以蚂蚁集团搜索推荐业务中台为例,搜索推荐业务将公共依赖、通用逻辑、流程引擎全部下沉到基座,并且定义了一些 SPI,搜索推荐算法由各个模块开发者实现,当前搜索推荐已经接入了 1000+ 模块,平均代码发布上线不到 1 天,真正实现了代码的 “朝写夕发”。

      +

      总结与规划

      +

      SOFAServerless 在蚂蚁集团历经 3 年多的演进与打磨,目前已在所有业务线完成落地,支撑了全集团 1/4 的在线流量,帮助全集团实现了功能平均上线从 12 天减少到 6 天、长尾应用服务器砍半秒级发布运维的极致降本增效结果。
      未来,蚂蚁集团会更大规模推广 SOFAServerless 研发模式,并持续建设弹性能力,做到更极致的弹性体验和绿色低碳。同时,我们也会重点投入开源能力建设,希望和社区同学共同打造极致的模块化技术体系,为各行各业的软件创造技术价值,助力企业实现降本提效。

      + + +
      + + + + + + +
      + + +
      +
      + 最后修改 October 30, 2023: update home (f751b32b) +
      + +
      + + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/user-cases/index.html b/docs/public/user-cases/index.html index ebe3f3656..f3ae9ab1f 100644 --- a/docs/public/user-cases/index.html +++ b/docs/public/user-cases/index.html @@ -1,12 +1,344 @@ -用户案例 | SOFAServerless - - + + + + + + + + + + + + + + + + + + + + + +用户案例 | SOFAServerless + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

      用户案例

      -

      最后修改 October 20, 2023: set type for user cases (0f022a65)
      - - \ No newline at end of file + + + +
      + +
      +
      +
      +
      + + +
      + + + + + +
      +

      用户案例

      + + + + + +
      + + + + + + +
      + +
      +
      + 最后修改 October 20, 2023: set type for user cases (0f022a65) +
      +
      + +
      +
      +
      +
      +
      +
      +
      + + + + + + + +
      +
      + + + + + + + +
      + +
      +
      +
      +
      + + + + + + + \ No newline at end of file diff --git a/docs/public/zh-cn/index.html b/docs/public/zh-cn/index.html index ada0f3dbf..c32fa80c5 100644 --- a/docs/public/zh-cn/index.html +++ b/docs/public/zh-cn/index.html @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/docs/public/zh-cn/sitemap.xml b/docs/public/zh-cn/sitemap.xml index ccab15e3e..d92273ff8 100644 --- a/docs/public/zh-cn/sitemap.xml +++ b/docs/public/zh-cn/sitemap.xml @@ -1 +1,275 @@ -/docs/contribution-guidelines/runtime/logj42/2023-11-17T23:49:47+08:00/docs/contribution-guidelines/runtime/ehcache/2023-11-19T20:12:43+08:00/blog/2023/09/22/%E5%B9%B2%E8%B4%A7%E6%96%87%E7%AB%A0%E4%B8%8E%E8%A7%86%E9%A2%91/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/arkctl/architecture/2023-10-27T17:04:54+08:00/docs/contribution-guidelines/arklet/architecture/2023-09-22T20:16:30+08:00/docs/contribution-guidelines/module-controller/architecture/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/contribution/local-dev-test/2023-11-03T17:53:20+08:00/docs/contribution-guidelines/our-vision/2023-10-26T18:06:20+08:00/docs/tutorials/base-create/springboot-and-sofaboot/2023-11-19T20:29:49+08:00/docs/tutorials/module-create/springboot-and-sofaboot/2023-11-27T15:29:07+08:00/docs/tutorials/module-development/coding-specification/2023-11-03T17:53:20+08:00/docs/introduction/2023-09-22T20:16:30+08:00/docs/introduction/architecture/arch-principle/2023-10-26T18:06:20+08:00/docs/introduction/intro-and-scenario/2023-11-02T13:48:30+08:00/docs/tutorials/module-operation/module-online-and-offline/2023-11-23T11:36:14+08:00/docs/faq/import-full-springboot-in-module/2023-10-26T18:06:20+08:00/docs/tutorials/module-create/init_by_archtype/2023-11-14T18:21:09+08:00/blog/2023/09/22/%E8%8E%B7%E5%A5%96%E6%83%85%E5%86%B5/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/module-controller/crd-definition/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/communication-channel/2023-11-14T17:58:40+08:00/docs/contribution-guidelines/arklet/how-to-release/2023-10-30T23:00:45+08:00/docs/contribution-guidelines/contribution/first-pr/2023-10-26T18:06:20+08:00/docs/introduction/architecture/class-delegation-principle/2023-10-27T12:38:51+08:00/docs/quick-start/2023-11-08T17:36:28+08:00/docs/tutorials/module-operation/module-deployment-and-rollback/2023-11-23T11:36:14+08:00/docs/tutorials/module-development/module-slimming/2023-11-15T18:15:40+08:00/docs/introduction/industry-background/2023-10-30T23:00:45+08:00/docs/video-training/2023-11-24T20:17:46+08:00/blog/2023/09/22/%E7%A4%BE%E5%8C%BA%E4%BC%9A%E8%AE%AE%E7%BA%AA%E8%A6%81/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/contribution/2023-09-22T20:16:30+08:00/docs/tutorials/module-operation/incompatible-base-and-module-upgrade/2023-10-26T18:06:20+08:00/docs/tutorials/base-create/2023-11-14T11:29:55+08:00/docs/introduction/architecture/2023-09-22T20:16:30+08:00/docs/tutorials/module-development/module-and-base-communication/2023-11-09T15:03:43+08:00/docs/tutorials/2023-09-22T20:16:30+08:00/docs/contribution-guidelines/2023-09-22T20:16:30+08:00/blog/2023/09/22/%E4%BA%A7%E5%93%81%E5%8F%91%E5%B8%83%E8%AE%B0%E5%BD%95/2023-10-12T12:02:37+08:00/docs/contribution-guidelines/module-controller/core-code-structure/2023-09-22T20:16:30+08:00/docs/contribution-guidelines/role-and-promotion/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/2023-10-26T18:06:20+08:00/docs/faq/2023-09-22T20:16:30+08:00/docs/tutorials/module-create/2023-11-14T11:29:55+08:00/docs/tutorials/module-operation/module-scale-and-replace/2023-11-23T11:36:14+08:00/docs/tutorials/module-development/module-dev-arkctl/2023-11-15T17:18:57+08:00/docs/tutorials/module-development/module-debug/2023-10-26T18:06:20+08:00/docs/contribution-guidelines/module-controller/module-lifecycle/2023-10-19T19:55:45+08:00/docs/tutorials/module-development/reuse-base-interceptor/2023-10-30T23:00:45+08:00/docs/tutorials/trial_step_by_step/2023-11-24T17:36:32+08:00/docs/contribution-guidelines/module-controller/sequence-diagram/2023-10-19T19:55:45+08:00/docs/contribution-guidelines/sofa-ark/2023-09-22T20:16:30+08:00/docs/contribution-guidelines/contribution/sermon/2023-09-22T20:16:30+08:00/docs/tutorials/module-development/reuse-base-datasource/2023-10-30T23:00:45+08:00/docs/tutorials/module-operation/operation-and-scheduling-strategy/2023-10-30T23:00:45+08:00/docs/tutorials/module-development/2023-11-14T11:29:55+08:00/docs/contribution-guidelines/arklet/2023-09-22T20:16:30+08:00/docs/tutorials/module-operation/arklet-standalone-usage/2023-09-22T20:16:30+08:00/docs/tutorials/module-development/static-merge-deployment/2023-11-23T20:22:40+08:00/docs/tutorials/module-operation/2023-11-14T11:29:55+08:00/docs/tutorials/module-operation/module-information-viewing/2023-11-06T15:31:31+08:00/docs/contribution-guidelines/module-controller/2023-09-22T20:16:30+08:00/docs/tutorials/module-operation/module-service/2023-11-23T11:36:14+08:00/docs/tutorials/module-development/runtime-compatibility-list/2023-11-23T11:37:35+08:00/docs/contribution-guidelines/arkctl/2023-09-22T20:16:30+08:00/docs/contribution-guidelines/runtime/2023-11-17T23:49:47+08:00/docs/tutorials/module-operation/crd-definition/2023-11-17T19:54:16+08:00/docs/tutorials/module-development/sofa-ark/2023-09-22T20:16:30+08:00/user-cases/ant-group/2023-10-30T23:00:45+08:00/user-cases/alibaba-aidc/2023-10-30T23:00:45+08:00/user-cases/all-users/2023-11-21T12:37:14+08:00/2023-11-03T17:53:20+08:00/home/2023-11-24T20:17:46+08:00/community/2023-11-03T19:46:25+08:00/blog/2023-09-22T20:16:30+08:00/categories//tags//docs/2023-11-03T17:53:20+08:00/search/2023-09-22T20:16:30+08:00/user-cases/2023-10-20T21:16:22+08:00 \ No newline at end of file + + + + /docs/contribution-guidelines/runtime/logj42/ + 2023-11-17T23:49:47+08:00 + + /docs/contribution-guidelines/runtime/ehcache/ + 2023-11-19T20:12:43+08:00 + + /docs/faq/faq/ + + /blog/2023/09/22/%E5%B9%B2%E8%B4%A7%E6%96%87%E7%AB%A0%E4%B8%8E%E8%A7%86%E9%A2%91/ + 2023-12-17T16:17:42+08:00 + + /docs/contribution-guidelines/arkctl/architecture/ + 2023-10-27T17:04:54+08:00 + + /docs/contribution-guidelines/arklet/architecture/ + 2023-12-08T18:56:22+08:00 + + /docs/contribution-guidelines/module-controller/architecture/ + 2023-10-26T18:06:20+08:00 + + /docs/contribution-guidelines/contribution/local-dev-test/ + 2023-11-03T17:53:20+08:00 + + /docs/contribution-guidelines/our-vision/ + 2023-10-26T18:06:20+08:00 + + /docs/tutorials/base-create/springboot-and-sofaboot/ + 2023-12-12T14:38:24+08:00 + + /docs/tutorials/module-create/springboot-and-sofaboot/ + 2023-11-27T15:29:07+08:00 + + /docs/tutorials/module-development/coding-specification/ + 2023-11-03T17:53:20+08:00 + + /docs/introduction/ + 2023-09-22T20:16:30+08:00 + + /docs/introduction/architecture/arch-principle/ + 2023-10-26T18:06:20+08:00 + + /docs/introduction/intro-and-scenario/ + 2023-11-02T13:48:30+08:00 + + /docs/tutorials/module-operation/module-online-and-offline/ + 2023-11-23T11:36:14+08:00 + + /docs/faq/import-full-springboot-in-module/ + 2023-10-26T18:06:20+08:00 + + /docs/tutorials/module-create/init_by_archtype/ + 2023-11-14T18:21:09+08:00 + + /blog/2023/09/22/%E8%8E%B7%E5%A5%96%E6%83%85%E5%86%B5/ + 2023-10-26T18:06:20+08:00 + + /docs/contribution-guidelines/module-controller/crd-definition/ + 2023-10-26T18:06:20+08:00 + + /docs/contribution-guidelines/communication-channel/ + 2023-11-14T17:58:40+08:00 + + /docs/contribution-guidelines/arklet/how-to-release/ + 2023-10-30T23:00:45+08:00 + + /docs/contribution-guidelines/contribution/first-pr/ + 2023-10-26T18:06:20+08:00 + + /docs/introduction/architecture/class-delegation-principle/ + 2023-10-27T12:38:51+08:00 + + /docs/quick-start/ + 2023-11-08T17:36:28+08:00 + + /docs/tutorials/module-operation/module-deployment-and-rollback/ + 2023-11-23T11:36:14+08:00 + + /docs/tutorials/module-development/module-slimming/ + 2023-12-12T14:38:24+08:00 + + /docs/introduction/industry-background/ + 2023-10-30T23:00:45+08:00 + + /docs/video-training/ + 2023-11-24T20:17:46+08:00 + + /blog/2023/09/22/%E7%A4%BE%E5%8C%BA%E4%BC%9A%E8%AE%AE%E7%BA%AA%E8%A6%81/ + 2023-10-26T18:06:20+08:00 + + /docs/contribution-guidelines/contribution/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-operation/incompatible-base-and-module-upgrade/ + 2023-10-26T18:06:20+08:00 + + /docs/tutorials/base-create/ + 2023-11-14T11:29:55+08:00 + + /docs/introduction/architecture/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-development/module-and-base-communication/ + 2023-11-09T15:03:43+08:00 + + /docs/tutorials/ + 2023-09-22T20:16:30+08:00 + + /docs/contribution-guidelines/ + 2023-09-22T20:16:30+08:00 + + /blog/2023/09/22/%E4%BA%A7%E5%93%81%E5%8F%91%E5%B8%83%E8%AE%B0%E5%BD%95/ + 2023-10-12T12:02:37+08:00 + + /docs/contribution-guidelines/module-controller/core-code-structure/ + 2023-09-22T20:16:30+08:00 + + /docs/contribution-guidelines/role-and-promotion/ + 2023-10-26T18:06:20+08:00 + + /docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/ + 2023-10-26T18:06:20+08:00 + + /docs/faq/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-development/module-dev-arkctl/ + 2023-12-19T11:12:43+08:00 + + /docs/tutorials/module-development/module-debug/ + 2023-12-19T11:12:43+08:00 + + /docs/tutorials/module-create/ + 2023-11-14T11:29:55+08:00 + + /docs/tutorials/module-operation/module-scale-and-replace/ + 2023-11-23T11:36:14+08:00 + + /docs/contribution-guidelines/module-controller/module-lifecycle/ + 2023-10-19T19:55:45+08:00 + + /docs/tutorials/module-development/reuse-base-interceptor/ + 2023-10-30T23:00:45+08:00 + + /docs/tutorials/trial_step_by_step/ + 2023-11-24T17:36:32+08:00 + + /docs/contribution-guidelines/module-controller/sequence-diagram/ + 2023-10-19T19:55:45+08:00 + + /docs/contribution-guidelines/sofa-ark/ + 2023-09-22T20:16:30+08:00 + + /docs/contribution-guidelines/contribution/sermon/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-development/reuse-base-datasource/ + 2023-12-08T18:21:46+08:00 + + /docs/tutorials/module-operation/operation-and-scheduling-strategy/ + 2023-10-30T23:00:45+08:00 + + /docs/tutorials/module-development/ + 2023-11-14T11:29:55+08:00 + + /docs/contribution-guidelines/arklet/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-operation/arklet-standalone-usage/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-development/static-merge-deployment/ + 2023-11-23T20:22:40+08:00 + + /docs/tutorials/module-operation/ + 2023-11-14T11:29:55+08:00 + + /docs/tutorials/module-operation/module-information-viewing/ + 2023-11-06T15:31:31+08:00 + + /docs/contribution-guidelines/module-controller/ + 2023-09-22T20:16:30+08:00 + + /docs/tutorials/module-operation/module-service/ + 2023-11-23T11:36:14+08:00 + + /docs/tutorials/module-development/runtime-compatibility-list/ + 2023-12-13T17:35:20+08:00 + + /docs/contribution-guidelines/arkctl/ + 2023-09-22T20:16:30+08:00 + + /docs/contribution-guidelines/runtime/ + 2023-11-17T23:49:47+08:00 + + /docs/tutorials/module-operation/crd-definition/ + 2023-11-17T19:54:16+08:00 + + /docs/tutorials/module-development/sofa-ark/ + 2023-09-22T20:16:30+08:00 + + /user-cases/ant-group/ + 2023-10-30T23:00:45+08:00 + + /user-cases/alibaba-aidc/ + 2023-10-30T23:00:45+08:00 + + /user-cases/all-users/ + 2023-11-21T12:37:14+08:00 + + / + 2023-11-03T17:53:20+08:00 + + + + /home/ + 2023-12-13T17:35:20+08:00 + + /community/ + 2023-11-03T19:46:25+08:00 + + /blog/ + 2023-09-22T20:16:30+08:00 + + /docs/contribution-guidelines/runtime/logback/ + 2023-12-12T17:55:19+08:00 + + /docs/tutorials/module-development/runtime-service-route/ + 2023-11-30T16:37:06+08:00 + + /categories/ + + + + /tags/ + + + + /docs/ + 2023-11-03T17:53:20+08:00 + + /search/ + 2023-09-22T20:16:30+08:00 + + /user-cases/ + 2023-10-20T21:16:22+08:00 + + From bf6ec2ad9373829b80acaf647806733bd9248d19 Mon Sep 17 00:00:00 2001 From: leojames Date: Fri, 22 Dec 2023 14:19:54 +0800 Subject: [PATCH 13/89] update docs --- .../runtime/logback.md | 0 .../module-development/module-dev-arkctl.md | 2 +- docs/public/docs/_print/index.html | 27 +------------------ .../contribution-guidelines/_print/index.html | 25 ----------------- .../arkctl/architecture/index.html | 2 -- .../contribution-guidelines/arkctl/index.html | 2 -- .../arklet/architecture/index.html | 2 -- .../arklet/how-to-release/index.html | 2 -- .../contribution-guidelines/arklet/index.html | 2 -- .../communication-channel/index.html | 2 -- .../contribution/first-pr/index.html | 2 -- .../index.html | 2 -- .../contribution/index.html | 2 -- .../contribution/local-dev-test/index.html | 2 -- .../contribution/sermon/index.html | 2 -- .../docs/contribution-guidelines/index.html | 2 -- .../module-controller/architecture/index.html | 2 -- .../core-code-structure/index.html | 2 -- .../crd-definition/index.html | 2 -- .../module-controller/index.html | 2 -- .../module-lifecycle/index.html | 2 -- .../sequence-diagram/index.html | 2 -- .../our-vision/index.html | 2 -- .../role-and-promotion/index.html | 2 -- .../runtime/_print/index.html | 25 ----------------- .../runtime/ehcache/index.html | 2 -- .../runtime/index.html | 10 ------- .../runtime/logj42/index.html | 2 -- .../sofa-ark/index.html | 2 -- docs/public/docs/faq/faq/index.html | 11 ++++---- .../index.html | 2 -- docs/public/docs/faq/index.html | 2 -- docs/public/docs/index.html | 2 -- .../architecture/arch-principle/index.html | 2 -- .../class-delegation-principle/index.html | 2 -- .../docs/introduction/architecture/index.html | 2 -- docs/public/docs/introduction/index.html | 2 -- .../industry-background/index.html | 2 -- .../intro-and-scenario/index.html | 2 -- docs/public/docs/quick-start/index.html | 2 -- docs/public/docs/tutorials/_print/index.html | 2 +- .../docs/tutorials/base-create/index.html | 2 -- .../springboot-and-sofaboot/index.html | 2 -- docs/public/docs/tutorials/index.html | 2 -- .../docs/tutorials/module-create/index.html | 2 -- .../module-create/init_by_archtype/index.html | 2 -- .../springboot-and-sofaboot/index.html | 2 -- .../module-development/_print/index.html | 2 +- .../coding-specification/index.html | 2 -- .../tutorials/module-development/index.html | 2 -- .../module-and-base-communication/index.html | 2 -- .../module-debug/index.html | 2 -- .../module-dev-arkctl/index.html | 18 ++++++------- .../module-slimming/index.html | 2 -- .../reuse-base-datasource/index.html | 2 -- .../reuse-base-interceptor/index.html | 2 -- .../runtime-compatibility-list/index.html | 2 -- .../runtime-service-route/index.html | 2 -- .../module-development/sofa-ark/index.html | 2 -- .../static-merge-deployment/index.html | 2 -- .../arklet-standalone-usage/index.html | 2 -- .../crd-definition/index.html | 2 -- .../index.html | 2 -- .../tutorials/module-operation/index.html | 2 -- .../module-deployment-and-rollback/index.html | 2 -- .../module-information-viewing/index.html | 2 -- .../module-online-and-offline/index.html | 2 -- .../module-scale-and-replace/index.html | 2 -- .../module-service/index.html | 2 -- .../index.html | 2 -- .../tutorials/trial_step_by_step/index.html | 2 -- docs/public/docs/video-training/index.html | 2 -- docs/public/sitemap.xml | 2 +- docs/public/zh-cn/sitemap.xml | 6 ++--- 74 files changed, 21 insertions(+), 233 deletions(-) delete mode 100644 docs/content/zh-cn/docs/contribution-guidelines/runtime/logback.md diff --git a/docs/content/zh-cn/docs/contribution-guidelines/runtime/logback.md b/docs/content/zh-cn/docs/contribution-guidelines/runtime/logback.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md b/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md index 9f7f8e318..314464419 100644 --- a/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md +++ b/docs/content/zh-cn/docs/tutorials/module-development/module-dev-arkctl.md @@ -12,7 +12,7 @@ weight: 400 方法二: -1. 在 [二进制列表]([https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0](https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0)) 中下载对应的二进制并加入到本地 +1. 在 [二进制列表]([https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0](https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0) 中下载对应的二进制并加入到本地 path 中。 ### 本地快速部署 diff --git a/docs/public/docs/_print/index.html b/docs/public/docs/_print/index.html index 6ddfd7311..2a86e5af0 100644 --- a/docs/public/docs/_print/index.html +++ b/docs/public/docs/_print/index.html @@ -806,14 +806,6 @@

      产品文档

      - - - -
    • 5.9.3:
    • - - - - @@ -2176,7 +2168,7 @@

      Arkctl 工具安装

      方法二:

        -
      1. 二进制列表 中下载对应的二进制并加入到本地 +
      2. 在 [二进制列表](https://github.com/sofastack/sofa-serverless/releases/tag/arkctl-release-0.1.0 中下载对应的二进制并加入到本地 path 中。

      本地快速部署

      @@ -4756,23 +4748,6 @@

      最佳实践的样例

      - - - - - -
      - -

      5.9.3 -

      - - -
      - - - - - - diff --git a/docs/public/docs/contribution-guidelines/_print/index.html b/docs/public/docs/contribution-guidelines/_print/index.html index 881f2878a..a38690fb9 100644 --- a/docs/public/docs/contribution-guidelines/_print/index.html +++ b/docs/public/docs/contribution-guidelines/_print/index.html @@ -396,14 +396,6 @@

      参与社区

      - - - -
    • 9.3:
    • - - - - @@ -1805,23 +1797,6 @@

      最佳实践的样例

      - - - - - -
      - -

      9.3 -

      - - -
      - - - - - - diff --git a/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html b/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html index 4ffde8e7e..f273891a6 100644 --- a/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html +++ b/docs/public/docs/contribution-guidelines/arkctl/architecture/index.html @@ -303,8 +303,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/arkctl/index.html b/docs/public/docs/contribution-guidelines/arkctl/index.html index 1d47413dd..49774a4e9 100644 --- a/docs/public/docs/contribution-guidelines/arkctl/index.html +++ b/docs/public/docs/contribution-guidelines/arkctl/index.html @@ -299,8 +299,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/arklet/architecture/index.html b/docs/public/docs/contribution-guidelines/arklet/architecture/index.html index 8f5a12ac9..e337c827e 100644 --- a/docs/public/docs/contribution-guidelines/arklet/architecture/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/architecture/index.html @@ -339,8 +339,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html b/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html index aed0fa600..757f0d659 100644 --- a/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/how-to-release/index.html @@ -335,8 +335,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/arklet/index.html b/docs/public/docs/contribution-guidelines/arklet/index.html index 442e1ce5c..baa3d513b 100644 --- a/docs/public/docs/contribution-guidelines/arklet/index.html +++ b/docs/public/docs/contribution-guidelines/arklet/index.html @@ -299,8 +299,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/communication-channel/index.html b/docs/public/docs/contribution-guidelines/communication-channel/index.html index 04d21a85a..4e64a3a7d 100644 --- a/docs/public/docs/contribution-guidelines/communication-channel/index.html +++ b/docs/public/docs/contribution-guidelines/communication-channel/index.html @@ -327,8 +327,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html b/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html index a78f82b87..6244336b3 100644 --- a/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/first-pr/index.html @@ -327,8 +327,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html b/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html index dac16048e..6a8c07ae3 100644 --- a/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/improve-doc-and-process-and-issue/index.html @@ -319,8 +319,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/contribution/index.html b/docs/public/docs/contribution-guidelines/contribution/index.html index a19872080..be980cbf5 100644 --- a/docs/public/docs/contribution-guidelines/contribution/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/index.html @@ -299,8 +299,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html b/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html index 22ecea330..9f40518d5 100644 --- a/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/local-dev-test/index.html @@ -331,8 +331,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/contribution/sermon/index.html b/docs/public/docs/contribution-guidelines/contribution/sermon/index.html index c724a65ee..83d393cb1 100644 --- a/docs/public/docs/contribution-guidelines/contribution/sermon/index.html +++ b/docs/public/docs/contribution-guidelines/contribution/sermon/index.html @@ -307,8 +307,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/index.html b/docs/public/docs/contribution-guidelines/index.html index 370677ab9..8450e3934 100644 --- a/docs/public/docs/contribution-guidelines/index.html +++ b/docs/public/docs/contribution-guidelines/index.html @@ -299,8 +299,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html b/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html index 0bd87687b..a76997c2c 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/architecture/index.html @@ -311,8 +311,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html b/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html index e0b8e052e..13d0dd4db 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/core-code-structure/index.html @@ -303,8 +303,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html b/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html index ca95eb1be..333364b7c 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/crd-definition/index.html @@ -303,8 +303,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/module-controller/index.html b/docs/public/docs/contribution-guidelines/module-controller/index.html index 4d61b6bb8..9a1bfdcba 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/index.html @@ -299,8 +299,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html b/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html index 12b86233c..3ac5c523c 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/module-lifecycle/index.html @@ -307,8 +307,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html b/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html index eb7988ccc..2a34123cf 100644 --- a/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html +++ b/docs/public/docs/contribution-guidelines/module-controller/sequence-diagram/index.html @@ -303,8 +303,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/our-vision/index.html b/docs/public/docs/contribution-guidelines/our-vision/index.html index 9120039b4..355f44b33 100644 --- a/docs/public/docs/contribution-guidelines/our-vision/index.html +++ b/docs/public/docs/contribution-guidelines/our-vision/index.html @@ -331,8 +331,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/role-and-promotion/index.html b/docs/public/docs/contribution-guidelines/role-and-promotion/index.html index a2bfaae3c..6c9645358 100644 --- a/docs/public/docs/contribution-guidelines/role-and-promotion/index.html +++ b/docs/public/docs/contribution-guidelines/role-and-promotion/index.html @@ -315,8 +315,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/runtime/_print/index.html b/docs/public/docs/contribution-guidelines/runtime/_print/index.html index 4980f8aa6..c4d93b1a6 100644 --- a/docs/public/docs/contribution-guidelines/runtime/_print/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/_print/index.html @@ -166,14 +166,6 @@

      多模块运行时适配或最佳实践

      - - - -
    • 3:
    • - - - - @@ -365,23 +357,6 @@

      最佳实践的样例

      - - - - - -
      - -

      3 -

      - - -
      - - - - - - diff --git a/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html b/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html index 950518143..06d470a97 100644 --- a/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/ehcache/index.html @@ -311,8 +311,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • diff --git a/docs/public/docs/contribution-guidelines/runtime/index.html b/docs/public/docs/contribution-guidelines/runtime/index.html index 3a9459bd5..d753debdb 100644 --- a/docs/public/docs/contribution-guidelines/runtime/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/index.html @@ -299,8 +299,6 @@ log4j2 的多模块化适配
    • ehcache 的多模块化最佳实践 -
    • -
    • @@ -377,14 +375,6 @@

      - -
      -
      - -
      -

      -
      -
      -
      - - - - - - -
      - - -
      - - -
      - - - - - -
      -
      -
      -
      - - - - - - - -
      -
      - - - - - - - -
      - -
      -
      -
      - - - - - - - - \ No newline at end of file diff --git a/docs/public/docs/contribution-guidelines/runtime/logj42/index.html b/docs/public/docs/contribution-guidelines/runtime/logj42/index.html index 7d5825470..71315cd1a 100644 --- a/docs/public/docs/contribution-guidelines/runtime/logj42/index.html +++ b/docs/public/docs/contribution-guidelines/runtime/logj42/index.html @@ -136,9 +136,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -