Repository: dragonflyoss/Dragonfly2
Pull request #750 (2021-10-25 05:47:28)

Add application
Signed-off-by: yxxhero <aiopsclub@163.com>

<!--- Provide a general summary of your changes in the Title above -->

## Description
add callsystem
<!--- Describe your changes in detail -->

## Related Issue
https://github.com/dragonflyoss/Dragonfly2/issues/678
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## Screenshots (if appropriate):

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] Code compiles correctly.
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.



Patch:
diff --git a/api/manager/docs.go b/api/manager/docs.go
index 3a8c1119..6a70588e 100644
--- a/api/manager/docs.go
+++ b/api/manager/docs.go
@@ -2342,6 +2342,279 @@ var doc = `{
                 }
             }
         },
+        "/security-groups/{id}/security-rules/{security_rule_id}": {
+            "put": {
+                "description": "Add SecurityRule to SecurityGroup",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityGroup"
+                ],
+                "summary": "Add SecurityRule to SecurityGroup",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "security rule id",
+                        "name": "security_rule_id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": ""
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            },
+            "delete": {
+                "description": "Destroy SecurityRule to SecurityGroup",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityGroup"
+                ],
+                "summary": "Destroy SecurityRule to SecurityGroup",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "security rule id",
+                        "name": "security_rule_id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": ""
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
+        "/security-rules": {
+            "get": {
+                "description": "Get SecurityRules",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Get SecurityRules",
+                "parameters": [
+                    {
+                        "type": "integer",
+                        "default": 0,
+                        "description": "current page",
+                        "name": "page",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "maximum": 50,
+                        "minimum": 2,
+                        "type": "integer",
+                        "default": 10,
+                        "description": "return max item count, default 10, max 50",
+                        "name": "per_page",
+                        "in": "query",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/definitions/model.SecurityRule"
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            },
+            "post": {
+                "description": "create by json config",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Create SecurityRule",
+                "parameters": [
+                    {
+                        "description": "SecurityRule",
+                        "name": "SecurityRule",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/types.CreateSecurityRuleRequest"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.SecurityRule"
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
+        "/security-rules/{id}": {
+            "get": {
+                "description": "Get SecurityRule by id",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Get SecurityRule",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.SecurityRule"
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            },
+            "patch": {
+                "description": "Update by json config",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Update SecurityRule",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "description": "SecurityRule",
+                        "name": "SecurityRule",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/types.UpdateSecurityRuleRequest"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.SecurityRule"
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
         "/securityGroups/{id}": {
             "delete": {
                 "description": "Destroy by id",
@@ -2380,6 +2653,44 @@ var doc = `{
                 }
             }
         },
+        "/securityRules/{id}": {
+            "delete": {
+                "description": "Destroy by id",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Destroy SecurityRule",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": ""
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
         "/user/signin/{name}": {
             "get": {
                 "description": "oauth signin by json config",
@@ -3026,6 +3337,32 @@ var doc = `{
             }
         },
         "model.SecurityGroup": {
+            "type": "object",
+            "properties": {
+                "bio": {
+                    "type": "string"
+                },
+                "created_at": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "integer"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "security_rules": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.SecurityRule"
+                    }
+                },
+                "updated_at": {
+                    "type": "string"
+                }
+            }
+        },
+        "model.SecurityRule": {
             "type": "object",
             "properties": {
                 "bio": {
@@ -3046,6 +3383,12 @@ var doc = `{
                 "proxy_domain": {
                     "type": "string"
                 },
+                "security_groups": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.SecurityGroup"
+                    }
+                },
                 "updated_at": {
                     "type": "string"
                 }
@@ -3142,9 +3485,6 @@ var doc = `{
                 },
                 "name": {
                     "type": "string"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3312,9 +3652,6 @@ var doc = `{
                 },
                 "scopes": {
                     "$ref": "#/definitions/types.SchedulerClusterScopes"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3356,6 +3693,20 @@ var doc = `{
             }
         },
         "types.CreateSecurityGroupRequest": {
+            "type": "object",
+            "required": [
+                "name"
+            ],
+            "properties": {
+                "bio": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.CreateSecurityRuleRequest": {
             "type": "object",
             "required": [
                 "domain",
@@ -3511,9 +3862,6 @@ var doc = `{
                 },
                 "name": {
                     "type": "string"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3611,9 +3959,6 @@ var doc = `{
                 },
                 "scopes": {
                     "$ref": "#/definitions/types.SchedulerClusterScopes"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3648,6 +3993,17 @@ var doc = `{
             }
         },
         "types.UpdateSecurityGroupRequest": {
+            "type": "object",
+            "properties": {
+                "bio": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.UpdateSecurityRuleRequest": {
             "type": "object",
             "properties": {
                 "bio": {
diff --git a/api/manager/swagger.json b/api/manager/swagger.json
index 9e55e8f5..c885d652 100644
--- a/api/manager/swagger.json
+++ b/api/manager/swagger.json
@@ -2328,6 +2328,279 @@
                 }
             }
         },
+        "/security-groups/{id}/security-rules/{security_rule_id}": {
+            "put": {
+                "description": "Add SecurityRule to SecurityGroup",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityGroup"
+                ],
+                "summary": "Add SecurityRule to SecurityGroup",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "security rule id",
+                        "name": "security_rule_id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": ""
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            },
+            "delete": {
+                "description": "Destroy SecurityRule to SecurityGroup",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityGroup"
+                ],
+                "summary": "Destroy SecurityRule to SecurityGroup",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "type": "string",
+                        "description": "security rule id",
+                        "name": "security_rule_id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": ""
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
+        "/security-rules": {
+            "get": {
+                "description": "Get SecurityRules",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Get SecurityRules",
+                "parameters": [
+                    {
+                        "type": "integer",
+                        "default": 0,
+                        "description": "current page",
+                        "name": "page",
+                        "in": "query",
+                        "required": true
+                    },
+                    {
+                        "maximum": 50,
+                        "minimum": 2,
+                        "type": "integer",
+                        "default": 10,
+                        "description": "return max item count, default 10, max 50",
+                        "name": "per_page",
+                        "in": "query",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/definitions/model.SecurityRule"
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            },
+            "post": {
+                "description": "create by json config",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Create SecurityRule",
+                "parameters": [
+                    {
+                        "description": "SecurityRule",
+                        "name": "SecurityRule",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/types.CreateSecurityRuleRequest"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.SecurityRule"
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
+        "/security-rules/{id}": {
+            "get": {
+                "description": "Get SecurityRule by id",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Get SecurityRule",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.SecurityRule"
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            },
+            "patch": {
+                "description": "Update by json config",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Update SecurityRule",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    },
+                    {
+                        "description": "SecurityRule",
+                        "name": "SecurityRule",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/types.UpdateSecurityRuleRequest"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.SecurityRule"
+                        }
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
         "/securityGroups/{id}": {
             "delete": {
                 "description": "Destroy by id",
@@ -2366,6 +2639,44 @@
                 }
             }
         },
+        "/securityRules/{id}": {
+            "delete": {
+                "description": "Destroy by id",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "SecurityRule"
+                ],
+                "summary": "Destroy SecurityRule",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "id",
+                        "name": "id",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": ""
+                    },
+                    "400": {
+                        "description": ""
+                    },
+                    "404": {
+                        "description": ""
+                    },
+                    "500": {
+                        "description": ""
+                    }
+                }
+            }
+        },
         "/user/signin/{name}": {
             "get": {
                 "description": "oauth signin by json config",
@@ -3012,6 +3323,32 @@
             }
         },
         "model.SecurityGroup": {
+            "type": "object",
+            "properties": {
+                "bio": {
+                    "type": "string"
+                },
+                "created_at": {
+                    "type": "string"
+                },
+                "id": {
+                    "type": "integer"
+                },
+                "name": {
+                    "type": "string"
+                },
+                "security_rules": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.SecurityRule"
+                    }
+                },
+                "updated_at": {
+                    "type": "string"
+                }
+            }
+        },
+        "model.SecurityRule": {
             "type": "object",
             "properties": {
                 "bio": {
@@ -3032,6 +3369,12 @@
                 "proxy_domain": {
                     "type": "string"
                 },
+                "security_groups": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.SecurityGroup"
+                    }
+                },
                 "updated_at": {
                     "type": "string"
                 }
@@ -3128,9 +3471,6 @@
                 },
                 "name": {
                     "type": "string"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3298,9 +3638,6 @@
                 },
                 "scopes": {
                     "$ref": "#/definitions/types.SchedulerClusterScopes"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3342,6 +3679,20 @@
             }
         },
         "types.CreateSecurityGroupRequest": {
+            "type": "object",
+            "required": [
+                "name"
+            ],
+            "properties": {
+                "bio": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.CreateSecurityRuleRequest": {
             "type": "object",
             "required": [
                 "domain",
@@ -3497,9 +3848,6 @@
                 },
                 "name": {
                     "type": "string"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3597,9 +3945,6 @@
                 },
                 "scopes": {
                     "$ref": "#/definitions/types.SchedulerClusterScopes"
-                },
-                "security_group_domain": {
-                    "type": "string"
                 }
             }
         },
@@ -3634,6 +3979,17 @@
             }
         },
         "types.UpdateSecurityGroupRequest": {
+            "type": "object",
+            "properties": {
+                "bio": {
+                    "type": "string"
+                },
+                "name": {
+                    "type": "string"
+                }
+            }
+        },
+        "types.UpdateSecurityRuleRequest": {
             "type": "object",
             "properties": {
                 "bio": {
diff --git a/api/manager/swagger.yaml b/api/manager/swagger.yaml
index f8402300..1ea6ddac 100644
--- a/api/manager/swagger.yaml
+++ b/api/manager/swagger.yaml
@@ -181,6 +181,23 @@ definitions:
         type: string
     type: object
   model.SecurityGroup:
+    properties:
+      bio:
+        type: string
+      created_at:
+        type: string
+      id:
+        type: integer
+      name:
+        type: string
+      security_rules:
+        items:
+          $ref: '#/definitions/model.SecurityRule'
+        type: array
+      updated_at:
+        type: string
+    type: object
+  model.SecurityRule:
     properties:
       bio:
         type: string
@@ -194,6 +211,10 @@ definitions:
         type: string
       proxy_domain:
         type: string
+      security_groups:
+        items:
+          $ref: '#/definitions/model.SecurityGroup'
+        type: array
       updated_at:
         type: string
     type: object
@@ -255,8 +276,6 @@ definitions:
         $ref: '#/definitions/types.CDNClusterConfig'
       name:
         type: string
-      security_group_domain:
-        type: string
     required:
     - config
     - name
@@ -370,8 +389,6 @@ definitions:
         type: string
       scopes:
         $ref: '#/definitions/types.SchedulerClusterScopes'
-      security_group_domain:
-        type: string
     required:
     - client_config
     - config
@@ -404,6 +421,15 @@ definitions:
     - scheduler_cluster_id
     type: object
   types.CreateSecurityGroupRequest:
+    properties:
+      bio:
+        type: string
+      name:
+        type: string
+    required:
+    - name
+    type: object
+  types.CreateSecurityRuleRequest:
     properties:
       bio:
         type: string
@@ -507,8 +533,6 @@ definitions:
         $ref: '#/definitions/types.CDNClusterConfig'
       name:
         type: string
-      security_group_domain:
-        type: string
     type: object
   types.UpdateCDNRequest:
     properties:
@@ -572,8 +596,6 @@ definitions:
         type: string
       scopes:
         $ref: '#/definitions/types.SchedulerClusterScopes'
-      security_group_domain:
-        type: string
     type: object
   types.UpdateSchedulerRequest:
     properties:
@@ -596,6 +618,13 @@ definitions:
         type: string
     type: object
   types.UpdateSecurityGroupRequest:
+    properties:
+      bio:
+        type: string
+      name:
+        type: string
+    type: object
+  types.UpdateSecurityRuleRequest:
     properties:
       bio:
         type: string
@@ -2161,6 +2190,189 @@ paths:
       summary: Add Scheduler to SecurityGroup
       tags:
       - SecurityGroup
+  /security-groups/{id}/security-rules/{security_rule_id}:
+    delete:
+      consumes:
+      - application/json
+      description: Destroy SecurityRule to SecurityGroup
+      parameters:
+      - description: id
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: security rule id
+        in: path
+        name: security_rule_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: ""
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Destroy SecurityRule to SecurityGroup
+      tags:
+      - SecurityGroup
+    put:
+      consumes:
+      - application/json
+      description: Add SecurityRule to SecurityGroup
+      parameters:
+      - description: id
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: security rule id
+        in: path
+        name: security_rule_id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: ""
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Add SecurityRule to SecurityGroup
+      tags:
+      - SecurityGroup
+  /security-rules:
+    get:
+      consumes:
+      - application/json
+      description: Get SecurityRules
+      parameters:
+      - default: 0
+        description: current page
+        in: query
+        name: page
+        required: true
+        type: integer
+      - default: 10
+        description: return max item count, default 10, max 50
+        in: query
+        maximum: 50
+        minimum: 2
+        name: per_page
+        required: true
+        type: integer
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/model.SecurityRule'
+            type: array
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Get SecurityRules
+      tags:
+      - SecurityRule
+    post:
+      consumes:
+      - application/json
+      description: create by json config
+      parameters:
+      - description: SecurityRule
+        in: body
+        name: SecurityRule
+        required: true
+        schema:
+          $ref: '#/definitions/types.CreateSecurityRuleRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/model.SecurityRule'
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Create SecurityRule
+      tags:
+      - SecurityRule
+  /security-rules/{id}:
+    get:
+      consumes:
+      - application/json
+      description: Get SecurityRule by id
+      parameters:
+      - description: id
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/model.SecurityRule'
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Get SecurityRule
+      tags:
+      - SecurityRule
+    patch:
+      consumes:
+      - application/json
+      description: Update by json config
+      parameters:
+      - description: id
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: SecurityRule
+        in: body
+        name: SecurityRule
+        required: true
+        schema:
+          $ref: '#/definitions/types.UpdateSecurityRuleRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/model.SecurityRule'
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Update SecurityRule
+      tags:
+      - SecurityRule
   /securityGroups/{id}:
     delete:
       consumes:
@@ -2186,6 +2398,31 @@ paths:
       summary: Destroy SecurityGroup
       tags:
       - SecurityGroup
+  /securityRules/{id}:
+    delete:
+      consumes:
+      - application/json
+      description: Destroy by id
+      parameters:
+      - description: id
+        in: path
+        name: id
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: ""
+        "400":
+          description: ""
+        "404":
+          description: ""
+        "500":
+          description: ""
+      summary: Destroy SecurityRule
+      tags:
+      - SecurityRule
   /user/signin/{name}:
     get:
       consumes:
diff --git a/docs/en/api-reference/api-reference.md b/docs/en/api-reference/api-reference.md
index 05382599..386a78f7 100644
--- a/docs/en/api-reference/api-reference.md
+++ b/docs/en/api-reference/api-reference.md
@@ -2337,6 +2337,268 @@ Add Scheduler to SecurityGroup
 * SecurityGroup
 
 
+<a name="api-v1-security-groups-id-security-rules-security_rule_id-put"></a>
+### Add SecurityRule to SecurityGroup
+```
+PUT /api/v1/security-groups/{id}/security-rules/{security_rule_id}
+```
+
+
+#### Description
+Add SecurityRule to SecurityGroup
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|
+|---|---|---|---|
+|**Path**|**id**  <br>*required*|id|string|
+|**Path**|**security_rule_id**  <br>*required*|security rule id|string|
+
+
+#### Responses
+
+|HTTP Code|Schema|
+|---|---|
+|**200**|No Content|
+|**400**|No Content|
+|**404**|No Content|
+|**500**|No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityGroup
+
+
+<a name="api-v1-security-groups-id-security-rules-security_rule_id-delete"></a>
+### Destroy SecurityRule to SecurityGroup
+```
+DELETE /api/v1/security-groups/{id}/security-rules/{security_rule_id}
+```
+
+
+#### Description
+Destroy SecurityRule to SecurityGroup
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|
+|---|---|---|---|
+|**Path**|**id**  <br>*required*|id|string|
+|**Path**|**security_rule_id**  <br>*required*|security rule id|string|
+
+
+#### Responses
+
+|HTTP Code|Schema|
+|---|---|
+|**200**|No Content|
+|**400**|No Content|
+|**404**|No Content|
+|**500**|No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityGroup
+
+
+<a name="api-v1-security-rules-post"></a>
+### Create SecurityRule
+```
+POST /api/v1/security-rules
+```
+
+
+#### Description
+create by json config
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|
+|---|---|---|---|
+|**Body**|**SecurityRule**  <br>*required*|SecurityRule|[types.CreateSecurityRuleRequest](#types-createsecurityrulerequest)|
+
+
+#### Responses
+
+|HTTP Code|Description|Schema|
+|---|---|---|
+|**200**|OK|[model.SecurityRule](#model-securityrule)|
+|**400**||No Content|
+|**404**||No Content|
+|**500**||No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityRule
+
+
+<a name="api-v1-security-rules-get"></a>
+### Get SecurityRules
+```
+GET /api/v1/security-rules
+```
+
+
+#### Description
+Get SecurityRules
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|Default|
+|---|---|---|---|---|
+|**Query**|**page**  <br>*required*|current page|integer|`0`|
+|**Query**|**per_page**  <br>*required*|return max item count, default 10, max 50|integer|`10`|
+
+
+#### Responses
+
+|HTTP Code|Description|Schema|
+|---|---|---|
+|**200**|OK|< [model.SecurityRule](#model-securityrule) > array|
+|**400**||No Content|
+|**404**||No Content|
+|**500**||No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityRule
+
+
+<a name="api-v1-security-rules-id-get"></a>
+### Get SecurityRule
+```
+GET /api/v1/security-rules/{id}
+```
+
+
+#### Description
+Get SecurityRule by id
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|
+|---|---|---|---|
+|**Path**|**id**  <br>*required*|id|string|
+
+
+#### Responses
+
+|HTTP Code|Description|Schema|
+|---|---|---|
+|**200**|OK|[model.SecurityRule](#model-securityrule)|
+|**400**||No Content|
+|**404**||No Content|
+|**500**||No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityRule
+
+
+<a name="api-v1-security-rules-id-patch"></a>
+### Update SecurityRule
+```
+PATCH /api/v1/security-rules/{id}
+```
+
+
+#### Description
+Update by json config
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|
+|---|---|---|---|
+|**Path**|**id**  <br>*required*|id|string|
+|**Body**|**SecurityRule**  <br>*required*|SecurityRule|[types.UpdateSecurityRuleRequest](#types-updatesecurityrulerequest)|
+
+
+#### Responses
+
+|HTTP Code|Description|Schema|
+|---|---|---|
+|**200**|OK|[model.SecurityRule](#model-securityrule)|
+|**400**||No Content|
+|**404**||No Content|
+|**500**||No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityRule
+
+
 <a name="api-v1-securitygroups-id-delete"></a>
 ### Destroy SecurityGroup
 ```
@@ -2380,6 +2642,49 @@ Destroy by id
 * SecurityGroup
 
 
+<a name="api-v1-securityrules-id-delete"></a>
+### Destroy SecurityRule
+```
+DELETE /api/v1/securityRules/{id}
+```
+
+
+#### Description
+Destroy by id
+
+
+#### Parameters
+
+|Type|Name|Description|Schema|
+|---|---|---|---|
+|**Path**|**id**  <br>*required*|id|string|
+
+
+#### Responses
+
+|HTTP Code|Schema|
+|---|---|
+|**200**|No Content|
+|**400**|No Content|
+|**404**|No Content|
+|**500**|No Content|
+
+
+#### Consumes
+
+* `application/json`
+
+
+#### Produces
+
+* `application/json`
+
+
+#### Tags
+
+* SecurityRule
+
+
 <a name="api-v1-user-signin-name-get"></a>
 ### Oauth Signin
 ```
@@ -2885,6 +3190,19 @@ Get User by id
 <a name="model-securitygroup"></a>
 ### model.SecurityGroup
 
+|Name|Schema|
+|---|---|
+|**bio**  <br>*optional*|string|
+|**created_at**  <br>*optional*|string|
+|**id**  <br>*optional*|integer|
+|**name**  <br>*optional*|string|
+|**security_rules**  <br>*optional*|< [model.SecurityRule](#model-securityrule) > array|
+|**updated_at**  <br>*optional*|string|
+
+
+<a name="model-securityrule"></a>
+### model.SecurityRule
+
 |Name|Schema|
 |---|---|
 |**bio**  <br>*optional*|string|
@@ -2893,6 +3211,7 @@ Get User by id
 |**id**  <br>*optional*|integer|
 |**name**  <br>*optional*|string|
 |**proxy_domain**  <br>*optional*|string|
+|**security_groups**  <br>*optional*|< [model.SecurityGroup](#model-securitygroup) > array|
 |**updated_at**  <br>*optional*|string|
 
 
@@ -2948,7 +3267,6 @@ Get User by id
 |**bio**  <br>*optional*|string|
 |**config**  <br>*required*|[types.CDNClusterConfig](#types-cdnclusterconfig)|
 |**name**  <br>*required*|string|
-|**security_group_domain**  <br>*optional*|string|
 
 
 <a name="types-createcdnrequest"></a>
@@ -3023,7 +3341,6 @@ Get User by id
 |**is_default**  <br>*optional*|boolean|
 |**name**  <br>*required*|string|
 |**scopes**  <br>*optional*|[types.SchedulerClusterScopes](#types-schedulerclusterscopes)|
-|**security_group_domain**  <br>*optional*|string|
 
 
 <a name="types-createschedulerrequest"></a>
@@ -3044,6 +3361,15 @@ Get User by id
 <a name="types-createsecuritygrouprequest"></a>
 ### types.CreateSecurityGroupRequest
 
+|Name|Schema|
+|---|---|
+|**bio**  <br>*optional*|string|
+|**name**  <br>*required*|string|
+
+
+<a name="types-createsecurityrulerequest"></a>
+### types.CreateSecurityRuleRequest
+
 |Name|Schema|
 |---|---|
 |**bio**  <br>*optional*|string|
@@ -3140,7 +3466,6 @@ Get User by id
 |**bio**  <br>*optional*|string|
 |**config**  <br>*optional*|[types.CDNClusterConfig](#types-cdnclusterconfig)|
 |**name**  <br>*optional*|string|
-|**security_group_domain**  <br>*optional*|string|
 
 
 <a name="types-updatecdnrequest"></a>
@@ -3200,7 +3525,6 @@ Get User by id
 |**is_default**  <br>*optional*|boolean|
 |**name**  <br>*optional*|string|
 |**scopes**  <br>*optional*|[types.SchedulerClusterScopes](#types-schedulerclusterscopes)|
-|**security_group_domain**  <br>*optional*|string|
 
 
 <a name="types-updateschedulerrequest"></a>
@@ -3221,6 +3545,15 @@ Get User by id
 <a name="types-updatesecuritygrouprequest"></a>
 ### types.UpdateSecurityGroupRequest
 
+|Name|Schema|
+|---|---|
+|**bio**  <br>*optional*|string|
+|**name**  <br>*optional*|string|
+
+
+<a name="types-updatesecurityrulerequest"></a>
+### types.UpdateSecurityRuleRequest
+
 |Name|Schema|
 |---|---|
 |**bio**  <br>*optional*|string|
diff --git a/docs/en/deployment/configuration/scheduler.yaml b/docs/en/deployment/configuration/scheduler.yaml
index cba15103..e20b0917 100644
--- a/docs/en/deployment/configuration/scheduler.yaml
+++ b/docs/en/deployment/configuration/scheduler.yaml
@@ -3,17 +3,13 @@
 
 # scheduler policy configuration
 scheduler:
-  # abtest whether to open ab test
-  # default: false
-  abtest: false
-  # aevaluator specify which evaluator is used when taskID suffix with _A
-  # only take effect when abtest is true
-  # default: ""
-  aevaluator: ""
-  # bevaluator specify which evaluator is used when taskID suffix with _B
-  # only take effect when abtest is true
-  # default: ""
-  bevaluator: ""
+  # algorithm configuration to use different scheduling algorithms,
+  # default configuration supports "default" and "ml"
+  # "default" is the rule-based scheduling algorithm, "ml" is the machine learning scheduling algorithm
+  # It also supports user plugin extension, the algorithm value is "plugin",
+  # and the compiled `d7y-scheduler-plugin-evaluator.so` file is added to
+  # the dragonfly working directory plugins
+  algorithm: default
   # workerNum is the number of goroutines that perform scheduling tasks
   # default:
   workerNum: 12
diff --git a/docs/zh-CN/api-reference/api-reference.md b/docs/zh-CN/api-reference/api-reference.md
index 9e53def3..4b8b3aea 100644
--- a/docs/zh-CN/api-reference/api-reference.md
+++ b/docs/zh-CN/api-reference/api-reference.md
@@ -2337,6 +2337,268 @@ Add Scheduler to SecurityGroup
 * SecurityGroup
 
 
+<a name="api-v1-security-groups-id-security-rules-security_rule_id-put"></a>
+### Add SecurityRule to SecurityGroup
+```
+PUT /api/v1/security-groups/{id}/security-rules/{security_rule_id}
+```
+
+
+#### 说明
+Add SecurityRule to SecurityGroup
+
+
+#### 参数
+
+|类型|名称|说明|类型|
+|---|---|---|---|
+|**Path**|**id**  <br>*必填*|id|string|
+|**Path**|**security_rule_id**  <br>*必填*|security rule id|string|
+
+
+#### 响应
+
+|HTTP代码|类型|
+|---|---|
+|**200**|无内容|
+|**400**|无内容|
+|**404**|无内容|
+|**500**|无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityGroup
+
+
+<a name="api-v1-security-groups-id-security-rules-security_rule_id-delete"></a>
+### Destroy SecurityRule to SecurityGroup
+```
+DELETE /api/v1/security-groups/{id}/security-rules/{security_rule_id}
+```
+
+
+#### 说明
+Destroy SecurityRule to SecurityGroup
+
+
+#### 参数
+
+|类型|名称|说明|类型|
+|---|---|---|---|
+|**Path**|**id**  <br>*必填*|id|string|
+|**Path**|**security_rule_id**  <br>*必填*|security rule id|string|
+
+
+#### 响应
+
+|HTTP代码|类型|
+|---|---|
+|**200**|无内容|
+|**400**|无内容|
+|**404**|无内容|
+|**500**|无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityGroup
+
+
+<a name="api-v1-security-rules-post"></a>
+### Create SecurityRule
+```
+POST /api/v1/security-rules
+```
+
+
+#### 说明
+create by json config
+
+
+#### 参数
+
+|类型|名称|说明|类型|
+|---|---|---|---|
+|**Body**|**SecurityRule**  <br>*必填*|SecurityRule|[types.CreateSecurityRuleRequest](#types-createsecurityrulerequest)|
+
+
+#### 响应
+
+|HTTP代码|说明|类型|
+|---|---|---|
+|**200**|OK|[model.SecurityRule](#model-securityrule)|
+|**400**||无内容|
+|**404**||无内容|
+|**500**||无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityRule
+
+
+<a name="api-v1-security-rules-get"></a>
+### Get SecurityRules
+```
+GET /api/v1/security-rules
+```
+
+
+#### 说明
+Get SecurityRules
+
+
+#### 参数
+
+|类型|名称|说明|类型|默认值|
+|---|---|---|---|---|
+|**Query**|**page**  <br>*必填*|current page|integer|`0`|
+|**Query**|**per_page**  <br>*必填*|return max item count, default 10, max 50|integer|`10`|
+
+
+#### 响应
+
+|HTTP代码|说明|类型|
+|---|---|---|
+|**200**|OK|< [model.SecurityRule](#model-securityrule) > array|
+|**400**||无内容|
+|**404**||无内容|
+|**500**||无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityRule
+
+
+<a name="api-v1-security-rules-id-get"></a>
+### Get SecurityRule
+```
+GET /api/v1/security-rules/{id}
+```
+
+
+#### 说明
+Get SecurityRule by id
+
+
+#### 参数
+
+|类型|名称|说明|类型|
+|---|---|---|---|
+|**Path**|**id**  <br>*必填*|id|string|
+
+
+#### 响应
+
+|HTTP代码|说明|类型|
+|---|---|---|
+|**200**|OK|[model.SecurityRule](#model-securityrule)|
+|**400**||无内容|
+|**404**||无内容|
+|**500**||无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityRule
+
+
+<a name="api-v1-security-rules-id-patch"></a>
+### Update SecurityRule
+```
+PATCH /api/v1/security-rules/{id}
+```
+
+
+#### 说明
+Update by json config
+
+
+#### 参数
+
+|类型|名称|说明|类型|
+|---|---|---|---|
+|**Path**|**id**  <br>*必填*|id|string|
+|**Body**|**SecurityRule**  <br>*必填*|SecurityRule|[types.UpdateSecurityRuleRequest](#types-updatesecurityrulerequest)|
+
+
+#### 响应
+
+|HTTP代码|说明|类型|
+|---|---|---|
+|**200**|OK|[model.SecurityRule](#model-securityrule)|
+|**400**||无内容|
+|**404**||无内容|
+|**500**||无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityRule
+
+
 <a name="api-v1-securitygroups-id-delete"></a>
 ### Destroy SecurityGroup
 ```
@@ -2380,6 +2642,49 @@ Destroy by id
 * SecurityGroup
 
 
+<a name="api-v1-securityrules-id-delete"></a>
+### Destroy SecurityRule
+```
+DELETE /api/v1/securityRules/{id}
+```
+
+
+#### 说明
+Destroy by id
+
+
+#### 参数
+
+|类型|名称|说明|类型|
+|---|---|---|---|
+|**Path**|**id**  <br>*必填*|id|string|
+
+
+#### 响应
+
+|HTTP代码|类型|
+|---|---|
+|**200**|无内容|
+|**400**|无内容|
+|**404**|无内容|
+|**500**|无内容|
+
+
+#### 消耗
+
+* `application/json`
+
+
+#### 生成
+
+* `application/json`
+
+
+#### 标签
+
+* SecurityRule
+
+
 <a name="api-v1-user-signin-name-get"></a>
 ### Oauth Signin
 ```
@@ -2885,6 +3190,19 @@ Get User by id
 <a name="model-securitygroup"></a>
 ### model.SecurityGroup
 
+|名称|类型|
+|---|---|
+|**bio**  <br>*可选*|string|
+|**created_at**  <br>*可选*|string|
+|**id**  <br>*可选*|integer|
+|**name**  <br>*可选*|string|
+|**security_rules**  <br>*可选*|< [model.SecurityRule](#model-securityrule) > array|
+|**updated_at**  <br>*可选*|string|
+
+
+<a name="model-securityrule"></a>
+### model.SecurityRule
+
 |名称|类型|
 |---|---|
 |**bio**  <br>*可选*|string|
@@ -2893,6 +3211,7 @@ Get User by id
 |**id**  <br>*可选*|integer|
 |**name**  <br>*可选*|string|
 |**proxy_domain**  <br>*可选*|string|
+|**security_groups**  <br>*可选*|< [model.SecurityGroup](#model-securitygroup) > array|
 |**updated_at**  <br>*可选*|string|
 
 
@@ -2948,7 +3267,6 @@ Get User by id
 |**bio**  <br>*可选*|string|
 |**config**  <br>*必填*|[types.CDNClusterConfig](#types-cdnclusterconfig)|
 |**name**  <br>*必填*|string|
-|**security_group_domain**  <br>*可选*|string|
 
 
 <a name="types-createcdnrequest"></a>
@@ -3023,7 +3341,6 @@ Get User by id
 |**is_default**  <br>*可选*|boolean|
 |**name**  <br>*必填*|string|
 |**scopes**  <br>*可选*|[types.SchedulerClusterScopes](#types-schedulerclusterscopes)|
-|**security_group_domain**  <br>*可选*|string|
 
 
 <a name="types-createschedulerrequest"></a>
@@ -3044,6 +3361,15 @@ Get User by id
 <a name="types-createsecuritygrouprequest"></a>
 ### types.CreateSecurityGroupRequest
 
+|名称|类型|
+|---|---|
+|**bio**  <br>*可选*|string|
+|**name**  <br>*必填*|string|
+
+
+<a name="types-createsecurityrulerequest"></a>
+### types.CreateSecurityRuleRequest
+
 |名称|类型|
 |---|---|
 |**bio**  <br>*可选*|string|
@@ -3140,7 +3466,6 @@ Get User by id
 |**bio**  <br>*可选*|string|
 |**config**  <br>*可选*|[types.CDNClusterConfig](#types-cdnclusterconfig)|
 |**name**  <br>*可选*|string|
-|**security_group_domain**  <br>*可选*|string|
 
 
 <a name="types-updatecdnrequest"></a>
@@ -3200,7 +3525,6 @@ Get User by id
 |**is_default**  <br>*可选*|boolean|
 |**name**  <br>*可选*|string|
 |**scopes**  <br>*可选*|[types.SchedulerClusterScopes](#types-schedulerclusterscopes)|
-|**security_group_domain**  <br>*可选*|string|
 
 
 <a name="types-updateschedulerrequest"></a>
@@ -3221,6 +3545,15 @@ Get User by id
 <a name="types-updatesecuritygrouprequest"></a>
 ### types.UpdateSecurityGroupRequest
 
+|名称|类型|
+|---|---|
+|**bio**  <br>*可选*|string|
+|**name**  <br>*可选*|string|
+
+
+<a name="types-updatesecurityrulerequest"></a>
+### types.UpdateSecurityRuleRequest
+
 |名称|类型|
 |---|---|
 |**bio**  <br>*可选*|string|
diff --git a/docs/zh-CN/deployment/configuration/scheduler.yaml b/docs/zh-CN/deployment/configuration/scheduler.yaml
index f659f253..19bfe631 100644
--- a/docs/zh-CN/deployment/configuration/scheduler.yaml
+++ b/docs/zh-CN/deployment/configuration/scheduler.yaml
@@ -2,17 +2,11 @@
 
 # scheduler 调度策略配置
 scheduler:
-  # abtest 是否开启ab测试
-  # default: false
-  abtest: false
-  # aevaluator 指定当开启AB测试时，后缀为 _A 的 taskID 使用的 evaluator 计算方法
-  # 只有当开启了 AB 测试的情况下该配置才生效
-  # default: ""
-  aevaluator: ""
-  # bevaluator 指定当开启AB测试时，后缀为 _B 的 taskID 使用的 evaluator 计算方法
-  # 只有当开启了 AB 测试的情况下该配置才生效
-  # default: ""
-  bevaluator: ""
+  # algorithm 使用不同调度算法配置，当前默认支持 "default" 和 "ml" 两种类型
+  # "default" 为基于规则的调度算法, "ml" 为基于机器学习的调度算法
+  # 也支持用户 plugin 扩展的方式，值为 "plugin"
+  # 并且在 dragonfly 工作目录 plugins 中添加编译好的 `d7y-scheduler-plugin-evaluator.so` 文件
+  algorithm: default
   # workerNum 执行调度任务处理的 goroutine 数量
   # default: 默认机器可用的 CPU 数量
   workerNum: 12
diff --git a/go.mod b/go.mod
index 9acf7e13..84826a0b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,9 @@ module d7y.io/dragonfly/v2
 go 1.15
 
 require (
-	github.com/HuKeping/rbtree v0.0.0-20210106022122-8ad34838eb2b
 	github.com/RichardKnop/machinery v1.10.6
 	github.com/VividCortex/mysqlerr v1.0.0
 	github.com/agiledragon/gomonkey v2.0.2+incompatible
-	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
 	github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible
 	github.com/appleboy/gin-jwt/v2 v2.6.5-0.20210827121450-79689222c755
 	github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
@@ -49,7 +47,6 @@ require (
 	github.com/onsi/ginkgo v1.16.5
 	github.com/onsi/gomega v1.14.0
 	github.com/opencontainers/go-digest v1.0.0
-	github.com/pborman/uuid v1.2.1
 	github.com/pelletier/go-toml v1.8.1 // indirect
 	github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index 7bc6cff3..2651987d 100644
--- a/go.sum
+++ b/go.sum
@@ -46,8 +46,6 @@ github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/HuKeping/rbtree v0.0.0-20210106022122-8ad34838eb2b h1:zDhQxG7rm8RLgLgi6NpfaVFsop+zxw5hwhbzHr624us=
-github.com/HuKeping/rbtree v0.0.0-20210106022122-8ad34838eb2b/go.mod h1:bODsl3NElqKlgf1UkBLj67fYmY5DsqkKrrYm/kMT/6Y=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@@ -70,7 +68,6 @@ github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3
 github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
 github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -382,7 +379,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
@@ -667,8 +663,6 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
-github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
-github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
 github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
diff --git a/internal/dfplugin/dfplugin.go b/internal/dfplugin/dfplugin.go
index a14eac9a..df7c905f 100644
--- a/internal/dfplugin/dfplugin.go
+++ b/internal/dfplugin/dfplugin.go
@@ -42,8 +42,9 @@ const (
 type PluginType string
 
 const (
-	PluginTypeResource = PluginType("resource")
-	PluginTypeManager  = PluginType("manager")
+	PluginTypeResource  = PluginType("resource")
+	PluginTypeManager   = PluginType("manager")
+	PluginTypeScheduler = PluginType("scheduler")
 )
 
 type PluginInitFunc func(option map[string]string) (plugin interface{}, meta map[string]string, err error)
diff --git a/internal/idgen/task_id.go b/internal/idgen/task_id.go
index cd5fc905..8b117029 100644
--- a/internal/idgen/task_id.go
+++ b/internal/idgen/task_id.go
@@ -17,7 +17,6 @@
 package idgen
 
 import (
-	"hash/crc32"
 	"strings"
 
 	"d7y.io/dragonfly/v2/pkg/rpc/base"
@@ -25,12 +24,7 @@ import (
 	"d7y.io/dragonfly/v2/pkg/util/net/urlutils"
 )
 
-const (
-	TwinsASuffix = "_A"
-	TwinsBSuffix = "_B"
-)
-
-// GenerateTaskID generates a taskId.
+// TaskID generates a taskId.
 // filter is separated by & character.
 func TaskID(url string, meta *base.UrlMeta) string {
 	var data []string
@@ -58,16 +52,3 @@ func TaskID(url string, meta *base.UrlMeta) string {
 
 	return digestutils.Sha256(data...)
 }
-
-// GenerateTwinsTaskId used A/B testing
-func TwinsTaskID(url string, meta *base.UrlMeta, peerID string) string {
-	taskID := TaskID(url, meta)
-
-	if crc32.ChecksumIEEE([]byte(peerID))&1 == 0 {
-		taskID += TwinsASuffix
-	} else {
-		taskID += TwinsBSuffix
-	}
-
-	return taskID
-}
diff --git a/manager/database/database.go b/manager/database/database.go
index f409af27..698e3de4 100644
--- a/manager/database/database.go
+++ b/manager/database/database.go
@@ -112,10 +112,12 @@ func migrate(db *gorm.DB) error {
 		&model.CDN{},
 		&model.SchedulerCluster{},
 		&model.Scheduler{},
+		&model.SecurityRule{},
 		&model.SecurityGroup{},
 		&model.User{},
 		&model.Oauth{},
 		&model.Config{},
+		&model.Application{},
 	)
 }
 
diff --git a/manager/handlers/application.go b/manager/handlers/application.go
new file mode 100644
index 00000000..6ddff1b7
--- /dev/null
+++ b/manager/handlers/application.go
@@ -0,0 +1,279 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package handlers
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+
+	// nolint
+	_ "d7y.io/dragonfly/v2/manager/model"
+	"d7y.io/dragonfly/v2/manager/types"
+)
+
+// @Summary Create Application
+// @Description create by json config
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param Application body types.CreateApplicationRequest true "Application"
+// @Success 200 {object} model.Application
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications [post]
+func (h *Handlers) CreateApplication(ctx *gin.Context) {
+	var json types.CreateApplicationRequest
+	if err := ctx.ShouldBindJSON(&json); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	application, err := h.service.CreateApplication(ctx.Request.Context(), json)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, application)
+}
+
+// @Summary Destroy Application
+// @Description Destroy by id
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id} [delete]
+func (h *Handlers) DestroyApplication(ctx *gin.Context) {
+	var params types.ApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.DestroyApplication(ctx.Request.Context(), params.ID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Update Application
+// @Description Update by json config
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param Application body types.UpdateApplicationRequest true "Application"
+// @Success 200 {object} model.Application
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id} [patch]
+func (h *Handlers) UpdateApplication(ctx *gin.Context) {
+	var params types.ApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	var json types.UpdateApplicationRequest
+	if err := ctx.ShouldBindJSON(&json); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	application, err := h.service.UpdateApplication(ctx.Request.Context(), params.ID, json)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, application)
+}
+
+// @Summary Get Application
+// @Description Get Application by id
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Success 200 {object} model.Application
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id} [get]
+func (h *Handlers) GetApplication(ctx *gin.Context) {
+	var params types.ApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	application, err := h.service.GetApplication(ctx.Request.Context(), params.ID)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, application)
+}
+
+// @Summary Get Applications
+// @Description Get Applications
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param page query int true "current page" default(0)
+// @Param per_page query int true "return max item count, default 10, max 50" default(10) minimum(2) maximum(50)
+// @Success 200 {object} []model.Application
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications [get]
+func (h *Handlers) GetApplications(ctx *gin.Context) {
+	var query types.GetApplicationsQuery
+	if err := ctx.ShouldBindQuery(&query); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	h.setPaginationDefault(&query.Page, &query.PerPage)
+	applications, count, err := h.service.GetApplications(ctx.Request.Context(), query)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	h.setPaginationLinkHeader(ctx, query.Page, query.PerPage, int(count))
+	ctx.JSON(http.StatusOK, applications)
+}
+
+// @Summary Add Scheduler to Application
+// @Description Add Scheduler to Application
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param scheduler_cluster_id path string true "scheduler cluster id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id}/scheduler-clusters/{scheduler_cluster_id} [put]
+func (h *Handlers) AddSchedulerClusterToApplication(ctx *gin.Context) {
+	var params types.AddSchedulerClusterToApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.AddSchedulerClusterToApplication(ctx.Request.Context(), params.ID, params.SchedulerClusterID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Delete Scheduler to Application
+// @Description Delete Scheduler to Application
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param scheduler_cluster_id path string true "scheduler cluster id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id}/scheduler-clusters/{scheduler_cluster_id} [delete]
+func (h *Handlers) DeleteSchedulerClusterToApplication(ctx *gin.Context) {
+	var params types.DeleteSchedulerClusterToApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.DeleteSchedulerClusterToApplication(ctx.Request.Context(), params.ID, params.SchedulerClusterID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Add CDN to Application
+// @Description Add CDN to Application
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param cdn_cluster_id path string true "cdn cluster id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id}/cdn-clusters/{cdn_cluster_id} [put]
+func (h *Handlers) AddCDNClusterToApplication(ctx *gin.Context) {
+	var params types.AddCDNClusterToApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.AddCDNClusterToApplication(ctx.Request.Context(), params.ID, params.CDNClusterID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Delete CDN to Application
+// @Description Delete CDN to Application
+// @Tags Application
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param cdn_cluster_id path string true "cdn cluster id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /applications/{id}/cdn-clusters/{cdn_cluster_id} [delete]
+func (h *Handlers) DeleteCDNClusterToApplication(ctx *gin.Context) {
+	var params types.DeleteCDNClusterToApplicationParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.DeleteCDNClusterToApplication(ctx.Request.Context(), params.ID, params.CDNClusterID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
diff --git a/manager/handlers/cdn_cluster.go b/manager/handlers/cdn_cluster.go
index 45900324..222a3ee3 100644
--- a/manager/handlers/cdn_cluster.go
+++ b/manager/handlers/cdn_cluster.go
@@ -44,17 +44,6 @@ func (h *Handlers) CreateCDNCluster(ctx *gin.Context) {
 		return
 	}
 
-	if json.SecurityGroupDomain != "" {
-		cdn, err := h.service.CreateCDNClusterWithSecurityGroupDomain(ctx.Request.Context(), json)
-		if err != nil {
-			ctx.Error(err) // nolint: errcheck
-			return
-		}
-
-		ctx.JSON(http.StatusOK, cdn)
-		return
-	}
-
 	cdnCluster, err := h.service.CreateCDNCluster(ctx.Request.Context(), json)
 	if err != nil {
 		ctx.Error(err) // nolint: errcheck
@@ -115,17 +104,6 @@ func (h *Handlers) UpdateCDNCluster(ctx *gin.Context) {
 		return
 	}
 
-	if json.SecurityGroupDomain != "" {
-		cdn, err := h.service.UpdateCDNClusterWithSecurityGroupDomain(ctx.Request.Context(), params.ID, json)
-		if err != nil {
-			ctx.Error(err) // nolint: errcheck
-			return
-		}
-
-		ctx.JSON(http.StatusOK, cdn)
-		return
-	}
-
 	cdnCluster, err := h.service.UpdateCDNCluster(ctx.Request.Context(), params.ID, json)
 	if err != nil {
 		ctx.Error(err) // nolint: errcheck
diff --git a/manager/handlers/scheduler_cluster.go b/manager/handlers/scheduler_cluster.go
index d55829ce..c56c1efc 100644
--- a/manager/handlers/scheduler_cluster.go
+++ b/manager/handlers/scheduler_cluster.go
@@ -44,17 +44,6 @@ func (h *Handlers) CreateSchedulerCluster(ctx *gin.Context) {
 		return
 	}
 
-	if json.SecurityGroupDomain != "" {
-		scheduler, err := h.service.CreateSchedulerClusterWithSecurityGroupDomain(ctx.Request.Context(), json)
-		if err != nil {
-			ctx.Error(err) // nolint: errcheck
-			return
-		}
-
-		ctx.JSON(http.StatusOK, scheduler)
-		return
-	}
-
 	schedulerCluster, err := h.service.CreateSchedulerCluster(ctx.Request.Context(), json)
 	if err != nil {
 		ctx.Error(err) // nolint: errcheck
@@ -115,17 +104,6 @@ func (h *Handlers) UpdateSchedulerCluster(ctx *gin.Context) {
 		return
 	}
 
-	if json.SecurityGroupDomain != "" {
-		scheduler, err := h.service.UpdateSchedulerClusterWithSecurityGroupDomain(ctx.Request.Context(), params.ID, json)
-		if err != nil {
-			ctx.Error(err) // nolint: errcheck
-			return
-		}
-
-		ctx.JSON(http.StatusOK, scheduler)
-		return
-	}
-
 	schedulerCluster, err := h.service.UpdateSchedulerCluster(ctx.Request.Context(), params.ID, json)
 	if err != nil {
 		ctx.Error(err) // nolint: errcheck
diff --git a/manager/handlers/security_group.go b/manager/handlers/security_group.go
index 99db28a8..4daa3ced 100644
--- a/manager/handlers/security_group.go
+++ b/manager/handlers/security_group.go
@@ -225,3 +225,59 @@ func (h *Handlers) AddCDNClusterToSecurityGroup(ctx *gin.Context) {
 
 	ctx.Status(http.StatusOK)
 }
+
+// @Summary Add SecurityRule to SecurityGroup
+// @Description Add SecurityRule to SecurityGroup
+// @Tags SecurityGroup
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param security_rule_id path string true "security rule id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /security-groups/{id}/security-rules/{security_rule_id} [put]
+func (h *Handlers) AddSecurityRuleToSecurityGroup(ctx *gin.Context) {
+	var params types.AddSecurityRuleToSecurityGroupParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	err := h.service.AddSecurityRuleToSecurityGroup(ctx.Request.Context(), params.ID, params.SecurityRuleID)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Destroy SecurityRule to SecurityGroup
+// @Description Destroy SecurityRule to SecurityGroup
+// @Tags SecurityGroup
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param security_rule_id path string true "security rule id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /security-groups/{id}/security-rules/{security_rule_id} [delete]
+func (h *Handlers) DestroySecurityRuleToSecurityGroup(ctx *gin.Context) {
+	var params types.AddSecurityRuleToSecurityGroupParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	err := h.service.DestroySecurityRuleToSecurityGroup(ctx.Request.Context(), params.ID, params.SecurityRuleID)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
diff --git a/manager/handlers/security_rule.go b/manager/handlers/security_rule.go
new file mode 100644
index 00000000..706999de
--- /dev/null
+++ b/manager/handlers/security_rule.go
@@ -0,0 +1,171 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package handlers
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+
+	// nolint
+	_ "d7y.io/dragonfly/v2/manager/model"
+	"d7y.io/dragonfly/v2/manager/types"
+)
+
+// @Summary Create SecurityRule
+// @Description create by json config
+// @Tags SecurityRule
+// @Accept json
+// @Produce json
+// @Param SecurityRule body types.CreateSecurityRuleRequest true "SecurityRule"
+// @Success 200 {object} model.SecurityRule
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /security-rules [post]
+func (h *Handlers) CreateSecurityRule(ctx *gin.Context) {
+	var json types.CreateSecurityRuleRequest
+	if err := ctx.ShouldBindJSON(&json); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	securityRule, err := h.service.CreateSecurityRule(ctx.Request.Context(), json)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, securityRule)
+}
+
+// @Summary Destroy SecurityRule
+// @Description Destroy by id
+// @Tags SecurityRule
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /securityRules/{id} [delete]
+func (h *Handlers) DestroySecurityRule(ctx *gin.Context) {
+	var params types.SecurityRuleParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.DestroySecurityRule(ctx.Request.Context(), params.ID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Update SecurityRule
+// @Description Update by json config
+// @Tags SecurityRule
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Param SecurityRule body types.UpdateSecurityRuleRequest true "SecurityRule"
+// @Success 200 {object} model.SecurityRule
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /security-rules/{id} [patch]
+func (h *Handlers) UpdateSecurityRule(ctx *gin.Context) {
+	var params types.SecurityRuleParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	var json types.UpdateSecurityRuleRequest
+	if err := ctx.ShouldBindJSON(&json); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	securityRule, err := h.service.UpdateSecurityRule(ctx.Request.Context(), params.ID, json)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, securityRule)
+}
+
+// @Summary Get SecurityRule
+// @Description Get SecurityRule by id
+// @Tags SecurityRule
+// @Accept json
+// @Produce json
+// @Param id path string true "id"
+// @Success 200 {object} model.SecurityRule
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /security-rules/{id} [get]
+func (h *Handlers) GetSecurityRule(ctx *gin.Context) {
+	var params types.SecurityRuleParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	securityRule, err := h.service.GetSecurityRule(ctx.Request.Context(), params.ID)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, securityRule)
+}
+
+// @Summary Get SecurityRules
+// @Description Get SecurityRules
+// @Tags SecurityRule
+// @Accept json
+// @Produce json
+// @Param page query int true "current page" default(0)
+// @Param per_page query int true "return max item count, default 10, max 50" default(10) minimum(2) maximum(50)
+// @Success 200 {object} []model.SecurityRule
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /security-rules [get]
+func (h *Handlers) GetSecurityRules(ctx *gin.Context) {
+	var query types.GetSecurityRulesQuery
+	if err := ctx.ShouldBindQuery(&query); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	h.setPaginationDefault(&query.Page, &query.PerPage)
+	securityRules, count, err := h.service.GetSecurityRules(ctx.Request.Context(), query)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	h.setPaginationLinkHeader(ctx, query.Page, query.PerPage, int(count))
+	ctx.JSON(http.StatusOK, securityRules)
+}
diff --git a/manager/model/application.go b/manager/model/application.go
new file mode 100644
index 00000000..f288643d
--- /dev/null
+++ b/manager/model/application.go
@@ -0,0 +1,29 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package model
+
+type Application struct {
+	Model
+	Name              string             `gorm:"column:name;type:varchar(256);index:uk_application_name,unique;not null;comment:name" json:"name"`
+	DownloadRateLimit string             `gorm:"column:download_rate_limit;type:varchar(1024);comment:download_rate_limit" json:"download_rate_limit"`
+	URL               string             `gorm:"column:url;not null;comment:url" json:"url"`
+	State             string             `gorm:"column:state;type:varchar(256);default:'enable';comment:state" json:"state"`
+	BIO               string             `gorm:"column:bio;type:varchar(1024);comment:biography" json:"bio"`
+	UserID            uint               `gorm:"comment:user id" json:"user_id"`
+	SchedulerClusters []SchedulerCluster `json:"-"`
+	CDNClusters       []CDNCluster       `json:"-"`
+}
diff --git a/manager/model/cdn_cluster.go b/manager/model/cdn_cluster.go
index ebc516ca..187640cc 100644
--- a/manager/model/cdn_cluster.go
+++ b/manager/model/cdn_cluster.go
@@ -25,6 +25,7 @@ type CDNCluster struct {
 	IsDefault         bool               `gorm:"column:is_default;not null;default:false;comment:default cdn cluster" json:"is_default"`
 	CDNs              []CDN              `json:"-"`
 	SecurityGroupID   uint               `gorm:"comment:security group id" json:"security_group_id"`
+	ApplicationID     uint               `gorm:"comment:application id" json:"application_id"`
 	SecurityGroup     SecurityGroup      `json:"-"`
 	Jobs              []Job              `gorm:"many2many:job_cdn_cluster;" json:"jobs"`
 }
diff --git a/manager/model/models.go b/manager/model/models.go
index 98d242cf..09eae526 100644
--- a/manager/model/models.go
+++ b/manager/model/models.go
@@ -42,7 +42,10 @@ func Paginate(page, perPage int) func(db *gorm.DB) *gorm.DB {
 	}
 }
 
-type JSONMap map[string]interface{}
+type (
+	JSONMap map[string]interface{}
+	Array   []string
+)
 
 func (m JSONMap) Value() (driver.Value, error) {
 	if m == nil {
@@ -52,6 +55,14 @@ func (m JSONMap) Value() (driver.Value, error) {
 	return string(ba), err
 }
 
+func (a Array) Value() (driver.Value, error) {
+	if a == nil {
+		return nil, nil
+	}
+	ba, err := a.MarshalJSON()
+	return string(ba), err
+}
+
 func (m *JSONMap) Scan(val interface{}) error {
 	var ba []byte
 	switch v := val.(type) {
@@ -68,6 +79,22 @@ func (m *JSONMap) Scan(val interface{}) error {
 	return err
 }
 
+func (a *Array) Scan(val interface{}) error {
+	var ba []byte
+	switch v := val.(type) {
+	case []byte:
+		ba = v
+	case string:
+		ba = []byte(v)
+	default:
+		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", val))
+	}
+	t := []string{}
+	err := json.Unmarshal(ba, &t)
+	*a = Array(t)
+	return err
+}
+
 func (m JSONMap) MarshalJSON() ([]byte, error) {
 	if m == nil {
 		return []byte("null"), nil
@@ -76,6 +103,14 @@ func (m JSONMap) MarshalJSON() ([]byte, error) {
 	return json.Marshal(t)
 }
 
+func (a Array) MarshalJSON() ([]byte, error) {
+	if a == nil {
+		return []byte("null"), nil
+	}
+	t := ([]string)(a)
+	return json.Marshal(t)
+}
+
 func (m *JSONMap) UnmarshalJSON(b []byte) error {
 	t := map[string]interface{}{}
 	err := json.Unmarshal(b, &t)
@@ -83,6 +118,13 @@ func (m *JSONMap) UnmarshalJSON(b []byte) error {
 	return err
 }
 
+func (a *Array) UnmarshalJSON(b []byte) error {
+	t := []string{}
+	err := json.Unmarshal(b, &t)
+	*a = Array(t)
+	return err
+}
+
 func (m JSONMap) GormDataType() string {
 	return "jsonmap"
 }
@@ -90,3 +132,11 @@ func (m JSONMap) GormDataType() string {
 func (JSONMap) GormDBDataType(db *gorm.DB, field *schema.Field) string {
 	return "text"
 }
+
+func (Array) GormDataType() string {
+	return "array"
+}
+
+func (Array) GormDBDataType(db *gorm.DB, field *schema.Field) string {
+	return "text"
+}
diff --git a/manager/model/scheduler_cluster.go b/manager/model/scheduler_cluster.go
index c0f7f512..d9fb0be1 100644
--- a/manager/model/scheduler_cluster.go
+++ b/manager/model/scheduler_cluster.go
@@ -29,4 +29,5 @@ type SchedulerCluster struct {
 	SecurityGroupID uint          `gorm:"comment:security group id" json:"security_group_id"`
 	SecurityGroup   SecurityGroup `json:"-"`
 	Jobs            []Job         `gorm:"many2many:job_scheduler_cluster;" json:"jobs"`
+	ApplicationID   uint          `gorm:"comment:application id" json:"application_id"`
 }
diff --git a/manager/model/security_group.go b/manager/model/security_group.go
index fb926947..bca3d383 100644
--- a/manager/model/security_group.go
+++ b/manager/model/security_group.go
@@ -20,8 +20,7 @@ type SecurityGroup struct {
 	Model
 	Name              string             `gorm:"column:name;type:varchar(256);index:uk_security_group_name,unique;not null;comment:name" json:"name"`
 	BIO               string             `gorm:"column:bio;type:varchar(1024);comment:biography" json:"bio"`
-	Domain            string             `gorm:"column:domain;type:varchar(256);index:uk_security_group_domain,unique;not null;comment:domain" json:"domain"`
-	ProxyDomain       string             `gorm:"column:proxy_domain;type:varchar(1024);comment:proxy domain" json:"proxy_domain"`
+	SecurityRules     []SecurityRule     `gorm:"many2many:security_group_security_rule;" json:"security_rules"`
 	SchedulerClusters []SchedulerCluster `json:"-"`
 	CDNClusters       []CDNCluster       `json:"-"`
 }
diff --git a/manager/model/security_rule.go b/manager/model/security_rule.go
new file mode 100644
index 00000000..256d46a3
--- /dev/null
+++ b/manager/model/security_rule.go
@@ -0,0 +1,26 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package model
+
+type SecurityRule struct {
+	Model
+	Name           string          `gorm:"column:name;type:varchar(256);index:uk_security_rule_name,unique;not null;comment:name" json:"name"`
+	BIO            string          `gorm:"column:bio;type:varchar(1024);comment:biography" json:"bio"`
+	Domain         string          `gorm:"column:domain;type:varchar(256);index:uk_security_rule_domain,unique;not null;comment:domain" json:"domain"`
+	ProxyDomain    string          `gorm:"column:proxy_domain;type:varchar(1024);comment:proxy domain" json:"proxy_domain"`
+	SecurityGroups []SecurityGroup `gorm:"many2many:security_group_security_rule;" json:"security_groups"`
+}
diff --git a/manager/router/router.go b/manager/router/router.go
index 12d506bf..267a697c 100644
--- a/manager/router/router.go
+++ b/manager/router/router.go
@@ -42,9 +42,7 @@ const (
 	OtelServiceName         = "dragonfly-manager"
 )
 
-var (
-	GinLogFileName = "gin.log"
-)
+var GinLogFileName = "gin.log"
 
 func Init(cfg *config.Config, service service.REST, enforcer *casbin.Enforcer) (*gin.Engine, error) {
 	// Set mode
@@ -147,6 +145,18 @@ func Init(cfg *config.Config, service service.REST, enforcer *casbin.Enforcer) (
 	s.GET(":id", h.GetScheduler)
 	s.GET("", h.GetSchedulers)
 
+	// Application
+	cs := apiv1.Group("/applications", jwt.MiddlewareFunc(), rbac)
+	cs.POST("", h.CreateApplication)
+	cs.DELETE(":id", h.DestroyApplication)
+	cs.PATCH(":id", h.UpdateApplication)
+	cs.GET(":id", h.GetApplication)
+	cs.GET("", h.GetApplications)
+	cs.PUT(":id/scheduler-clusters/:scheduler_cluster_id", h.AddSchedulerClusterToApplication)
+	cs.DELETE(":id/scheduler-clusters/:scheduler_cluster_id", h.DeleteSchedulerClusterToApplication)
+	cs.PUT(":id/cdn-clusters/:cdn_cluster_id", h.AddCDNClusterToApplication)
+	cs.DELETE(":id/cdn-clusters/:cdn_cluster_id", h.DeleteCDNClusterToApplication)
+
 	// CDN Cluster
 	cc := apiv1.Group("/cdn-clusters", jwt.MiddlewareFunc(), rbac)
 	cc.POST("", h.CreateCDNCluster)
@@ -165,6 +175,14 @@ func Init(cfg *config.Config, service service.REST, enforcer *casbin.Enforcer) (
 	c.GET(":id", h.GetCDN)
 	c.GET("", h.GetCDNs)
 
+	// Security Rule
+	sr := apiv1.Group("/security-rules", jwt.MiddlewareFunc(), rbac)
+	sr.POST("", h.CreateSecurityRule)
+	sr.DELETE(":id", h.DestroySecurityRule)
+	sr.PATCH(":id", h.UpdateSecurityRule)
+	sr.GET(":id", h.GetSecurityRule)
+	sr.GET("", h.GetSecurityRules)
+
 	// Security Group
 	sg := apiv1.Group("/security-groups", jwt.MiddlewareFunc(), rbac)
 	sg.POST("", h.CreateSecurityGroup)
@@ -174,6 +192,8 @@ func Init(cfg *config.Config, service service.REST, enforcer *casbin.Enforcer) (
 	sg.GET("", h.GetSecurityGroups)
 	sg.PUT(":id/scheduler-clusters/:scheduler_cluster_id", h.AddSchedulerClusterToSecurityGroup)
 	sg.PUT(":id/cdn-clusters/:cdn_cluster_id", h.AddCDNClusterToSecurityGroup)
+	sg.PUT(":id/security-rules/:security_rule_id", h.AddSecurityRuleToSecurityGroup)
+	sg.DELETE(":id/security-rules/:security_rule_id", h.DestroySecurityRuleToSecurityGroup)
 
 	// Config
 	config := apiv1.Group("/configs")
diff --git a/manager/searcher/searcher.go b/manager/searcher/searcher.go
index 8deeec52..50e95454 100644
--- a/manager/searcher/searcher.go
+++ b/manager/searcher/searcher.go
@@ -17,28 +17,56 @@
 package searcher
 
 import (
-	"sort"
+	"strings"
 
 	"github.com/mitchellh/mapstructure"
-	"gonum.org/v1/gonum/stat"
 
 	"d7y.io/dragonfly/v2/manager/model"
+	"d7y.io/dragonfly/v2/pkg/util/mathutils"
 )
 
 const (
+	// Condition IDC key
+	conditionIDC = "idc"
+
+	// Condition location key
+	conditionLocation = "location"
+
+	// Condition netTopology key
+	conditionNetTopology = "net_topology"
+
+	// Condition security domain key
 	conditionSecurityDomain = "security_domain"
-	conditionLocation       = "location"
-	conditionIDC            = "idc"
 )
 
 const (
-	conditionLocationWeight = 0.7
-	conditionIDCWeight      = 0.3
+	// IDC affinity weight
+	idcAffinityWeight float64 = 0.5
+
+	// NetTopology affinity weight
+	netTopologyAffinityWeight = 0.3
+
+	// Location affinity weight
+	locationAffinityWeight = 0.2
+)
+
+const (
+	// Maximum score
+	maxScore float64 = 1.0
+
+	// Minimum score
+	minScore = 0
+)
+
+const (
+	// Maximum number of elements
+	maxElementLen = 5
 )
 
 type Scopes struct {
-	Location []string `mapstructure:"location"`
-	IDC      []string `mapstructure:"idc"`
+	IDC         string `mapstructure:"idc"`
+	Location    string `mapstructure:"location"`
+	NetTopology string `mapstructure:"net_topology"`
 }
 
 type Searcher interface {
@@ -62,63 +90,96 @@ func (s *searcher) FindSchedulerCluster(schedulerClusters []model.SchedulerClust
 	}
 
 	// If there are security domain conditions, match clusters of the same security domain.
-	// If the security domain condition does not exist, it matches clusters that does not have a security domain.
+	// If the security domain condition does not exist, it will match all scheduler security domains.
 	// Then use clusters sets to score according to scopes.
-	securityDomain := conditions[conditionSecurityDomain]
 	var clusters []model.SchedulerCluster
-	for _, v := range schedulerClusters {
-		if v.SecurityGroup.Domain == securityDomain {
-			clusters = append(clusters, v)
+	securityDomain := conditions[conditionSecurityDomain]
+	for _, schedulerCluster := range schedulerClusters {
+		if len(schedulerCluster.Schedulers) > 0 {
+			if securityDomain == "" {
+				clusters = append(clusters, schedulerCluster)
+			} else {
+				for _, securityRule := range schedulerCluster.SecurityGroup.SecurityRules {
+					if strings.Compare(securityRule.Domain, securityDomain) == 0 {
+						clusters = append(clusters, schedulerCluster)
+					}
+				}
+			}
 		}
 	}
 
 	switch len(clusters) {
 	case 0:
+		// If the security domain does not match, there is no cluster available
 		return model.SchedulerCluster{}, false
 	case 1:
+		// If only one cluster matches the security domain, return the cluster directly
 		return clusters[0], true
 	default:
-		var maxMean float64 = 0
-		cluster := clusters[0]
-		for _, v := range clusters {
-			mean := calculateSchedulerClusterMean(conditions, v.Scopes)
-			if mean > maxMean {
-				maxMean = mean
-				cluster = v
+		// If there are multiple clusters matching the security domain,
+		// select the schuelder cluster with a higher score
+		var maxScore float64 = 0
+		result := clusters[0]
+		for _, cluster := range clusters {
+			var scopes Scopes
+			if err := mapstructure.Decode(cluster.Scopes, &scopes); err != nil {
+				// Scopes parse failed to skip this evaluation
+				continue
+			}
+
+			score := evaluate(conditions, scopes)
+			if score > maxScore {
+				maxScore = score
+				result = cluster
 			}
 		}
-		return cluster, true
+		return result, true
 	}
 }
 
-func calculateSchedulerClusterMean(conditions map[string]string, rawScopes map[string]interface{}) float64 {
-	var scopes Scopes
-	if err := mapstructure.Decode(rawScopes, &scopes); err != nil {
-		return 0
-	}
-
-	location := conditions[conditionLocation]
-	lx := calculateConditionScore(location, scopes.Location)
+// Evaluate the degree of matching between scheduler cluster and dfdaemon
+func evaluate(conditions map[string]string, scopes Scopes) float64 {
+	return idcAffinityWeight*calculateIDCAffinityScore(conditions[conditionIDC], scopes.IDC) +
+		locationAffinityWeight*calculateMultiElementAffinityScore(conditions[conditionLocation], scopes.Location) +
+		netTopologyAffinityWeight*calculateMultiElementAffinityScore(conditions[conditionNetTopology], scopes.NetTopology)
+}
 
-	idc := conditions[conditionIDC]
-	ix := calculateConditionScore(idc, scopes.IDC)
+// calculateIDCAffinityScore 0.0~1.0 larger and better
+func calculateIDCAffinityScore(dst, src string) float64 {
+	if dst != "" && src != "" && strings.Compare(dst, src) == 0 {
+		return maxScore
+	}
 
-	return stat.Mean([]float64{lx, ix}, []float64{conditionLocationWeight, conditionIDCWeight})
+	return minScore
 }
 
-func calculateConditionScore(value string, scope []string) float64 {
-	if value == "" {
-		return 0
+// calculateMultiElementAffinityScore 0.0~1.0 larger and better
+func calculateMultiElementAffinityScore(dst, src string) float64 {
+	if dst == "" || src == "" {
+		return minScore
+	}
+
+	if strings.Compare(dst, src) == 0 {
+		return maxScore
 	}
 
-	if len(scope) <= 0 {
-		return 0
+	// Calculate the number of multi-element matches divided by "|"
+	var score, elementLen int
+	dstElements := strings.Split(dst, "|")
+	srcElements := strings.Split(src, "|")
+	elementLen = mathutils.MaxInt(len(dstElements), len(srcElements))
+
+	// Maximum element length is 5
+	if elementLen > maxElementLen {
+		elementLen = maxElementLen
 	}
 
-	i := sort.SearchStrings(scope, value)
-	if i < 0 {
-		return 0
+	for i := 0; i < elementLen; i++ {
+		if strings.Compare(dstElements[i], srcElements[i]) != 0 {
+			break
+		}
+		score++
 	}
 
-	return 1
+	return float64(score) / float64(maxElementLen)
 }
diff --git a/manager/service/application.go b/manager/service/application.go
new file mode 100644
index 00000000..de91e36d
--- /dev/null
+++ b/manager/service/application.go
@@ -0,0 +1,161 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package service
+
+import (
+	"context"
+
+	"d7y.io/dragonfly/v2/manager/model"
+	"d7y.io/dragonfly/v2/manager/types"
+)
+
+func (s *rest) CreateApplication(ctx context.Context, json types.CreateApplicationRequest) (*model.Application, error) {
+	application := model.Application{
+		Name:              json.Name,
+		DownloadRateLimit: json.DownloadRateLimit,
+		URL:               json.URL,
+		UserID:            json.UserID,
+		BIO:               json.BIO,
+		State:             json.State,
+	}
+
+	if err := s.db.WithContext(ctx).Create(&application).Error; err != nil {
+		return nil, err
+	}
+
+	return &application, nil
+}
+
+func (s *rest) DestroyApplication(ctx context.Context, id uint) error {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).First(&application, id).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.WithContext(ctx).Unscoped().Delete(&model.Application{}, id).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *rest) UpdateApplication(ctx context.Context, id uint, json types.UpdateApplicationRequest) (*model.Application, error) {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).First(&application, id).Updates(model.Application{
+		Name:              json.Name,
+		DownloadRateLimit: json.DownloadRateLimit,
+		URL:               json.URL,
+		State:             json.State,
+		BIO:               json.BIO,
+		UserID:            json.UserID,
+	}).Error; err != nil {
+		return nil, err
+	}
+
+	return &application, nil
+}
+
+func (s *rest) GetApplication(ctx context.Context, id uint) (*model.Application, error) {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).Preload("SchedulerClusters").First(&application, id).Error; err != nil {
+		return nil, err
+	}
+
+	return &application, nil
+}
+
+func (s *rest) GetApplications(ctx context.Context, q types.GetApplicationsQuery) (*[]model.Application, int64, error) {
+	var count int64
+	applications := []model.Application{}
+	if err := s.db.WithContext(ctx).Scopes(model.Paginate(q.Page, q.PerPage)).Preload("SchedulerClusters").Find(&applications).Count(&count).Error; err != nil {
+		return nil, 0, err
+	}
+
+	return &applications, count, nil
+}
+
+func (s *rest) AddSchedulerClusterToApplication(ctx context.Context, id, schedulerClusterID uint) error {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).First(&application, id).Error; err != nil {
+		return err
+	}
+
+	schedulerCluster := model.SchedulerCluster{}
+	if err := s.db.WithContext(ctx).First(&schedulerCluster, schedulerClusterID).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.WithContext(ctx).Model(&application).Association("SchedulerClusters").Append(&schedulerCluster); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *rest) DeleteSchedulerClusterToApplication(ctx context.Context, id, schedulerClusterID uint) error {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).First(&application, id).Error; err != nil {
+		return err
+	}
+
+	schedulerCluster := model.SchedulerCluster{}
+	if err := s.db.WithContext(ctx).First(&schedulerCluster, schedulerClusterID).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.Model(&application).Association("SchedulerClusters").Delete(&schedulerCluster); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *rest) AddCDNClusterToApplication(ctx context.Context, id, cdnClusterID uint) error {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).First(&application, id).Error; err != nil {
+		return err
+	}
+
+	cdnCluster := model.CDNCluster{}
+	if err := s.db.WithContext(ctx).First(&cdnCluster, cdnClusterID).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.WithContext(ctx).Model(&application).Association("CDNClusters").Append(&cdnCluster); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *rest) DeleteCDNClusterToApplication(ctx context.Context, id, cdnClusterID uint) error {
+	application := model.Application{}
+	if err := s.db.WithContext(ctx).First(&application, id).Error; err != nil {
+		return err
+	}
+
+	cdnCluster := model.CDNCluster{}
+	if err := s.db.WithContext(ctx).First(&cdnCluster, cdnClusterID).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.Model(&application).Association("CDNClusters").Delete(&cdnCluster); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/manager/service/cdn_cluster.go b/manager/service/cdn_cluster.go
index 87f80452..596475e0 100644
--- a/manager/service/cdn_cluster.go
+++ b/manager/service/cdn_cluster.go
@@ -56,33 +56,6 @@ func (s *rest) DestroyCDNCluster(ctx context.Context, id uint) error {
 	return nil
 }
 
-func (s *rest) CreateCDNClusterWithSecurityGroupDomain(ctx context.Context, json types.CreateCDNClusterRequest) (*model.CDNCluster, error) {
-	securityGroup := model.SecurityGroup{
-		Domain: json.SecurityGroupDomain,
-	}
-	if err := s.db.WithContext(ctx).First(&securityGroup).Error; err != nil {
-		return s.CreateCDNCluster(ctx, json)
-	}
-
-	config, err := structutils.StructToMap(json.Config)
-	if err != nil {
-		return nil, err
-	}
-
-	cdnCluster := model.CDNCluster{
-		Name:   json.Name,
-		BIO:    json.BIO,
-		Config: config,
-	}
-
-	if err := s.db.WithContext(ctx).Model(&securityGroup).Association("CDNClusters").Append(&cdnCluster); err != nil {
-		return nil, err
-
-	}
-
-	return &cdnCluster, nil
-}
-
 func (s *rest) UpdateCDNCluster(ctx context.Context, id uint, json types.UpdateCDNClusterRequest) (*model.CDNCluster, error) {
 	config, err := structutils.StructToMap(json.Config)
 	if err != nil {
@@ -101,32 +74,6 @@ func (s *rest) UpdateCDNCluster(ctx context.Context, id uint, json types.UpdateC
 	return &cdnCluster, nil
 }
 
-func (s *rest) UpdateCDNClusterWithSecurityGroupDomain(ctx context.Context, id uint, json types.UpdateCDNClusterRequest) (*model.CDNCluster, error) {
-	securityGroup := model.SecurityGroup{
-		Domain: json.SecurityGroupDomain,
-	}
-	if err := s.db.WithContext(ctx).First(&securityGroup).Error; err != nil {
-		return s.UpdateCDNCluster(ctx, id, json)
-	}
-
-	config, err := structutils.StructToMap(json.Config)
-	if err != nil {
-		return nil, err
-	}
-
-	cdnCluster := model.CDNCluster{
-		Name:   json.Name,
-		BIO:    json.BIO,
-		Config: config,
-	}
-
-	if err := s.db.WithContext(ctx).Model(&securityGroup).Association("CDNClusters").Append(&cdnCluster); err != nil {
-		return nil, err
-	}
-
-	return &cdnCluster, nil
-}
-
 func (s *rest) GetCDNCluster(ctx context.Context, id uint) (*model.CDNCluster, error) {
 	cdnCluster := model.CDNCluster{}
 	if err := s.db.WithContext(ctx).First(&cdnCluster, id).Error; err != nil {
diff --git a/manager/service/scheduler_cluster.go b/manager/service/scheduler_cluster.go
index a77fe374..247ff31e 100644
--- a/manager/service/scheduler_cluster.go
+++ b/manager/service/scheduler_cluster.go
@@ -62,51 +62,6 @@ func (s *rest) CreateSchedulerCluster(ctx context.Context, json types.CreateSche
 	return &schedulerCluster, nil
 }
 
-func (s *rest) CreateSchedulerClusterWithSecurityGroupDomain(ctx context.Context, json types.CreateSchedulerClusterRequest) (*model.SchedulerCluster, error) {
-	securityGroup := model.SecurityGroup{
-		Domain: json.SecurityGroupDomain,
-	}
-	if err := s.db.WithContext(ctx).First(&securityGroup).Error; err != nil {
-		return s.CreateSchedulerCluster(ctx, json)
-	}
-
-	config, err := structutils.StructToMap(json.Config)
-	if err != nil {
-		return nil, err
-	}
-
-	clientConfig, err := structutils.StructToMap(json.ClientConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	scopes, err := structutils.StructToMap(json.Scopes)
-	if err != nil {
-		return nil, err
-	}
-
-	schedulerCluster := model.SchedulerCluster{
-		Name:         json.Name,
-		BIO:          json.BIO,
-		Config:       config,
-		ClientConfig: clientConfig,
-		Scopes:       scopes,
-		IsDefault:    json.IsDefault,
-	}
-
-	if err := s.db.WithContext(ctx).Model(&securityGroup).Association("SchedulerClusters").Append(&schedulerCluster); err != nil {
-		return nil, err
-	}
-
-	if json.CDNClusterID > 0 {
-		if err := s.AddSchedulerClusterToCDNCluster(ctx, json.CDNClusterID, schedulerCluster.ID); err != nil {
-			return nil, err
-		}
-	}
-
-	return &schedulerCluster, nil
-}
-
 func (s *rest) DestroySchedulerCluster(ctx context.Context, id uint) error {
 	schedulerCluster := model.SchedulerCluster{}
 	if err := s.db.WithContext(ctx).First(&schedulerCluster, id).Error; err != nil {
@@ -157,51 +112,6 @@ func (s *rest) UpdateSchedulerCluster(ctx context.Context, id uint, json types.U
 	return &schedulerCluster, nil
 }
 
-func (s *rest) UpdateSchedulerClusterWithSecurityGroupDomain(ctx context.Context, id uint, json types.UpdateSchedulerClusterRequest) (*model.SchedulerCluster, error) {
-	securityGroup := model.SecurityGroup{
-		Domain: json.SecurityGroupDomain,
-	}
-	if err := s.db.WithContext(ctx).First(&securityGroup).Error; err != nil {
-		return s.UpdateSchedulerCluster(ctx, id, json)
-	}
-
-	config, err := structutils.StructToMap(json.Config)
-	if err != nil {
-		return nil, err
-	}
-
-	clientConfig, err := structutils.StructToMap(json.ClientConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	scopes, err := structutils.StructToMap(json.Scopes)
-	if err != nil {
-		return nil, err
-	}
-
-	schedulerCluster := model.SchedulerCluster{
-		Name:         json.Name,
-		BIO:          json.BIO,
-		Config:       config,
-		ClientConfig: clientConfig,
-		Scopes:       scopes,
-		IsDefault:    json.IsDefault,
-	}
-
-	if err := s.db.WithContext(ctx).Model(&securityGroup).Association("SchedulerClusters").Append(&schedulerCluster); err != nil {
-		return nil, err
-	}
-
-	if json.CDNClusterID > 0 {
-		if err := s.AddSchedulerClusterToCDNCluster(ctx, json.CDNClusterID, schedulerCluster.ID); err != nil {
-			return nil, err
-		}
-	}
-
-	return &schedulerCluster, nil
-}
-
 func (s *rest) GetSchedulerCluster(ctx context.Context, id uint) (*model.SchedulerCluster, error) {
 	schedulerCluster := model.SchedulerCluster{}
 	if err := s.db.WithContext(ctx).Preload("CDNClusters").First(&schedulerCluster, id).Error; err != nil {
diff --git a/manager/service/security_group.go b/manager/service/security_group.go
index 635438a7..f0788b1e 100644
--- a/manager/service/security_group.go
+++ b/manager/service/security_group.go
@@ -25,10 +25,8 @@ import (
 
 func (s *rest) CreateSecurityGroup(ctx context.Context, json types.CreateSecurityGroupRequest) (*model.SecurityGroup, error) {
 	securityGroup := model.SecurityGroup{
-		Name:        json.Name,
-		BIO:         json.BIO,
-		Domain:      json.Domain,
-		ProxyDomain: json.ProxyDomain,
+		Name: json.Name,
+		BIO:  json.BIO,
 	}
 
 	if err := s.db.WithContext(ctx).Create(&securityGroup).Error; err != nil {
@@ -54,10 +52,8 @@ func (s *rest) DestroySecurityGroup(ctx context.Context, id uint) error {
 func (s *rest) UpdateSecurityGroup(ctx context.Context, id uint, json types.UpdateSecurityGroupRequest) (*model.SecurityGroup, error) {
 	securityGroup := model.SecurityGroup{}
 	if err := s.db.WithContext(ctx).First(&securityGroup, id).Updates(model.SecurityGroup{
-		Name:        json.Name,
-		BIO:         json.BIO,
-		Domain:      json.Domain,
-		ProxyDomain: json.ProxyDomain,
+		Name: json.Name,
+		BIO:  json.BIO,
 	}).Error; err != nil {
 		return nil, err
 	}
@@ -67,7 +63,7 @@ func (s *rest) UpdateSecurityGroup(ctx context.Context, id uint, json types.Upda
 
 func (s *rest) GetSecurityGroup(ctx context.Context, id uint) (*model.SecurityGroup, error) {
 	securityGroup := model.SecurityGroup{}
-	if err := s.db.WithContext(ctx).First(&securityGroup, id).Error; err != nil {
+	if err := s.db.WithContext(ctx).Preload("SecurityRules").First(&securityGroup, id).Error; err != nil {
 		return nil, err
 	}
 
@@ -78,9 +74,8 @@ func (s *rest) GetSecurityGroups(ctx context.Context, q types.GetSecurityGroupsQ
 	var count int64
 	var securityGroups []model.SecurityGroup
 	if err := s.db.WithContext(ctx).Scopes(model.Paginate(q.Page, q.PerPage)).Where(&model.SecurityGroup{
-		Name:   q.Name,
-		Domain: q.Domain,
-	}).Find(&securityGroups).Count(&count).Error; err != nil {
+		Name: q.Name,
+	}).Preload("SecurityRules").Find(&securityGroups).Count(&count).Error; err != nil {
 		return nil, 0, err
 	}
 
@@ -122,3 +117,39 @@ func (s *rest) AddCDNClusterToSecurityGroup(ctx context.Context, id, cdnClusterI
 
 	return nil
 }
+
+func (s *rest) AddSecurityRuleToSecurityGroup(ctx context.Context, id, securityRuleID uint) error {
+	securityGroup := model.SecurityGroup{}
+	if err := s.db.WithContext(ctx).First(&securityGroup, id).Error; err != nil {
+		return err
+	}
+
+	securityRule := model.SecurityRule{}
+	if err := s.db.WithContext(ctx).First(&securityRule, securityRuleID).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.WithContext(ctx).Model(&securityGroup).Association("SecurityRules").Append(&securityRule); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *rest) DestroySecurityRuleToSecurityGroup(ctx context.Context, id, securityRuleID uint) error {
+	securityGroup := model.SecurityGroup{}
+	if err := s.db.WithContext(ctx).First(&securityGroup, id).Error; err != nil {
+		return err
+	}
+
+	securityRule := model.SecurityRule{}
+	if err := s.db.WithContext(ctx).First(&securityRule, securityRuleID).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.WithContext(ctx).Model(&securityGroup).Association("SecurityRules").Delete(&securityRule); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/manager/service/security_rule.go b/manager/service/security_rule.go
new file mode 100644
index 00000000..0cf8a6a1
--- /dev/null
+++ b/manager/service/security_rule.go
@@ -0,0 +1,87 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package service
+
+import (
+	"context"
+
+	"d7y.io/dragonfly/v2/manager/model"
+	"d7y.io/dragonfly/v2/manager/types"
+)
+
+func (s *rest) CreateSecurityRule(ctx context.Context, json types.CreateSecurityRuleRequest) (*model.SecurityRule, error) {
+	securityRule := model.SecurityRule{
+		Name:        json.Name,
+		BIO:         json.BIO,
+		Domain:      json.Domain,
+		ProxyDomain: json.ProxyDomain,
+	}
+
+	if err := s.db.WithContext(ctx).Create(&securityRule).Error; err != nil {
+		return nil, err
+	}
+
+	return &securityRule, nil
+}
+
+func (s *rest) DestroySecurityRule(ctx context.Context, id uint) error {
+	securityRule := model.SecurityRule{}
+	if err := s.db.WithContext(ctx).First(&securityRule, id).Error; err != nil {
+		return err
+	}
+
+	if err := s.db.WithContext(ctx).Unscoped().Delete(&model.SecurityRule{}, id).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *rest) UpdateSecurityRule(ctx context.Context, id uint, json types.UpdateSecurityRuleRequest) (*model.SecurityRule, error) {
+	securityRule := model.SecurityRule{}
+	if err := s.db.WithContext(ctx).First(&securityRule, id).Updates(model.SecurityRule{
+		Name:        json.Name,
+		BIO:         json.BIO,
+		Domain:      json.Domain,
+		ProxyDomain: json.ProxyDomain,
+	}).Error; err != nil {
+		return nil, err
+	}
+
+	return &securityRule, nil
+}
+
+func (s *rest) GetSecurityRule(ctx context.Context, id uint) (*model.SecurityRule, error) {
+	securityRule := model.SecurityRule{}
+	if err := s.db.WithContext(ctx).First(&securityRule, id).Error; err != nil {
+		return nil, err
+	}
+
+	return &securityRule, nil
+}
+
+func (s *rest) GetSecurityRules(ctx context.Context, q types.GetSecurityRulesQuery) (*[]model.SecurityRule, int64, error) {
+	var count int64
+	var securityRules []model.SecurityRule
+	if err := s.db.WithContext(ctx).Scopes(model.Paginate(q.Page, q.PerPage)).Where(&model.SecurityRule{
+		Name: q.Name,
+	}).Find(&securityRules).Count(&count).Error; err != nil {
+		return nil, 0, err
+	}
+
+	return &securityRules, count, nil
+}
diff --git a/manager/service/service.go b/manager/service/service.go
index c1dfaa18..8a773e7b 100644
--- a/manager/service/service.go
+++ b/manager/service/service.go
@@ -60,10 +60,8 @@ type REST interface {
 	GetOauths(context.Context, types.GetOauthsQuery) (*[]model.Oauth, int64, error)
 
 	CreateCDNCluster(context.Context, types.CreateCDNClusterRequest) (*model.CDNCluster, error)
-	CreateCDNClusterWithSecurityGroupDomain(context.Context, types.CreateCDNClusterRequest) (*model.CDNCluster, error)
 	DestroyCDNCluster(context.Context, uint) error
 	UpdateCDNCluster(context.Context, uint, types.UpdateCDNClusterRequest) (*model.CDNCluster, error)
-	UpdateCDNClusterWithSecurityGroupDomain(context.Context, uint, types.UpdateCDNClusterRequest) (*model.CDNCluster, error)
 	GetCDNCluster(context.Context, uint) (*model.CDNCluster, error)
 	GetCDNClusters(context.Context, types.GetCDNClustersQuery) (*[]model.CDNCluster, int64, error)
 	AddCDNToCDNCluster(context.Context, uint, uint) error
@@ -76,10 +74,8 @@ type REST interface {
 	GetCDNs(context.Context, types.GetCDNsQuery) (*[]model.CDN, int64, error)
 
 	CreateSchedulerCluster(context.Context, types.CreateSchedulerClusterRequest) (*model.SchedulerCluster, error)
-	CreateSchedulerClusterWithSecurityGroupDomain(context.Context, types.CreateSchedulerClusterRequest) (*model.SchedulerCluster, error)
 	DestroySchedulerCluster(context.Context, uint) error
 	UpdateSchedulerCluster(context.Context, uint, types.UpdateSchedulerClusterRequest) (*model.SchedulerCluster, error)
-	UpdateSchedulerClusterWithSecurityGroupDomain(context.Context, uint, types.UpdateSchedulerClusterRequest) (*model.SchedulerCluster, error)
 	GetSchedulerCluster(context.Context, uint) (*model.SchedulerCluster, error)
 	GetSchedulerClusters(context.Context, types.GetSchedulerClustersQuery) (*[]model.SchedulerCluster, int64, error)
 	AddSchedulerToSchedulerCluster(context.Context, uint, uint) error
@@ -90,6 +86,12 @@ type REST interface {
 	GetScheduler(context.Context, uint) (*model.Scheduler, error)
 	GetSchedulers(context.Context, types.GetSchedulersQuery) (*[]model.Scheduler, int64, error)
 
+	CreateSecurityRule(context.Context, types.CreateSecurityRuleRequest) (*model.SecurityRule, error)
+	DestroySecurityRule(context.Context, uint) error
+	UpdateSecurityRule(context.Context, uint, types.UpdateSecurityRuleRequest) (*model.SecurityRule, error)
+	GetSecurityRule(context.Context, uint) (*model.SecurityRule, error)
+	GetSecurityRules(context.Context, types.GetSecurityRulesQuery) (*[]model.SecurityRule, int64, error)
+
 	CreateSecurityGroup(context.Context, types.CreateSecurityGroupRequest) (*model.SecurityGroup, error)
 	DestroySecurityGroup(context.Context, uint) error
 	UpdateSecurityGroup(context.Context, uint, types.UpdateSecurityGroupRequest) (*model.SecurityGroup, error)
@@ -97,6 +99,8 @@ type REST interface {
 	GetSecurityGroups(context.Context, types.GetSecurityGroupsQuery) (*[]model.SecurityGroup, int64, error)
 	AddSchedulerClusterToSecurityGroup(context.Context, uint, uint) error
 	AddCDNClusterToSecurityGroup(context.Context, uint, uint) error
+	AddSecurityRuleToSecurityGroup(context.Context, uint, uint) error
+	DestroySecurityRuleToSecurityGroup(context.Context, uint, uint) error
 
 	CreateConfig(context.Context, types.CreateConfigRequest) (*model.Config, error)
 	DestroyConfig(context.Context, uint) error
@@ -112,6 +116,16 @@ type REST interface {
 
 	CreateV1Preheat(context.Context, types.CreateV1PreheatRequest) (*types.CreateV1PreheatResponse, error)
 	GetV1Preheat(context.Context, string) (*types.GetV1PreheatResponse, error)
+
+	CreateApplication(context.Context, types.CreateApplicationRequest) (*model.Application, error)
+	DestroyApplication(context.Context, uint) error
+	UpdateApplication(context.Context, uint, types.UpdateApplicationRequest) (*model.Application, error)
+	GetApplication(context.Context, uint) (*model.Application, error)
+	GetApplications(context.Context, types.GetApplicationsQuery) (*[]model.Application, int64, error)
+	AddSchedulerClusterToApplication(context.Context, uint, uint) error
+	DeleteSchedulerClusterToApplication(context.Context, uint, uint) error
+	AddCDNClusterToApplication(context.Context, uint, uint) error
+	DeleteCDNClusterToApplication(context.Context, uint, uint) error
 }
 
 type rest struct {
diff --git a/manager/service/service_grpc.go b/manager/service/service_grpc.go
index d09e739d..28b94be5 100644
--- a/manager/service/service_grpc.go
+++ b/manager/service/service_grpc.go
@@ -66,7 +66,7 @@ func (s *GRPC) GetCDN(ctx context.Context, req *manager.GetCDNRequest) (*manager
 	// Cache Miss
 	logger.Infof("%s cache miss", cacheKey)
 	cdn := model.CDN{}
-	if err := s.db.WithContext(ctx).Preload("CDNCluster.SecurityGroup").First(&cdn, &model.CDN{
+	if err := s.db.WithContext(ctx).Preload("CDNCluster").First(&cdn, &model.CDN{
 		HostName:     req.HostName,
 		CDNClusterID: uint(req.CdnClusterId),
 	}).Error; err != nil {
@@ -93,13 +93,6 @@ func (s *GRPC) GetCDN(ctx context.Context, req *manager.GetCDNRequest) (*manager
 			Name:   cdn.CDNCluster.Name,
 			Bio:    cdn.CDNCluster.BIO,
 			Config: config,
-			SecurityGroup: &manager.SecurityGroup{
-				Id:          uint64(cdn.CDNCluster.SecurityGroup.ID),
-				Name:        cdn.CDNCluster.SecurityGroup.Name,
-				Bio:         cdn.CDNCluster.SecurityGroup.BIO,
-				Domain:      cdn.CDNCluster.SecurityGroup.Domain,
-				ProxyDomain: cdn.CDNCluster.SecurityGroup.ProxyDomain,
-			},
 		},
 	}
 
@@ -197,7 +190,7 @@ func (s *GRPC) GetScheduler(ctx context.Context, req *manager.GetSchedulerReques
 	// Cache Miss
 	logger.Infof("%s cache miss", cacheKey)
 	scheduler := model.Scheduler{}
-	if err := s.db.WithContext(ctx).Preload("SchedulerCluster.SecurityGroup").Preload("SchedulerCluster.CDNClusters.CDNs", &model.CDN{
+	if err := s.db.WithContext(ctx).Preload("SchedulerCluster").Preload("SchedulerCluster.CDNClusters.CDNs", &model.CDN{
 		Status: model.CDNStatusActive,
 	}).First(&scheduler, &model.Scheduler{
 		HostName:           req.HostName,
@@ -266,13 +259,6 @@ func (s *GRPC) GetScheduler(ctx context.Context, req *manager.GetSchedulerReques
 			Bio:          scheduler.SchedulerCluster.BIO,
 			Config:       schedulerClusterConfig,
 			ClientConfig: schedulerClusterClientConfig,
-			SecurityGroup: &manager.SecurityGroup{
-				Id:          uint64(scheduler.SchedulerCluster.SecurityGroup.ID),
-				Name:        scheduler.SchedulerCluster.SecurityGroup.Name,
-				Bio:         scheduler.SchedulerCluster.SecurityGroup.BIO,
-				Domain:      scheduler.SchedulerCluster.SecurityGroup.Domain,
-				ProxyDomain: scheduler.SchedulerCluster.SecurityGroup.ProxyDomain,
-			},
 		},
 		Cdns: pbCDNs,
 	}
@@ -391,7 +377,7 @@ func (s *GRPC) ListSchedulers(ctx context.Context, req *manager.ListSchedulersRe
 	// Cache Miss
 	logger.Infof("%s cache miss", cacheKey)
 	var schedulerClusters []model.SchedulerCluster
-	if err := s.db.WithContext(ctx).Preload("SecurityGroup").Find(&schedulerClusters).Error; err != nil {
+	if err := s.db.WithContext(ctx).Preload("SecurityGroup.SecurityRules").Preload("Schedulers", "status = ?", "active").Find(&schedulerClusters).Error; err != nil {
 		return nil, status.Error(codes.Unknown, err.Error())
 	}
 
diff --git a/manager/types/application.go b/manager/types/application.go
new file mode 100644
index 00000000..3f1f7f57
--- /dev/null
+++ b/manager/types/application.go
@@ -0,0 +1,64 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package types
+
+type ApplicationParams struct {
+	ID uint `uri:"id" binding:"required"`
+}
+
+type AddSchedulerClusterToApplicationParams struct {
+	ID                 uint `uri:"id" binding:"required"`
+	SchedulerClusterID uint `uri:"scheduler_cluster_id" binding:"required"`
+}
+
+type DeleteSchedulerClusterToApplicationParams struct {
+	ID                 uint `uri:"id" binding:"required"`
+	SchedulerClusterID uint `uri:"scheduler_cluster_id" binding:"required"`
+}
+
+type AddCDNClusterToApplicationParams struct {
+	ID           uint `uri:"id" binding:"required"`
+	CDNClusterID uint `uri:"cdn_cluster_id" binding:"required"`
+}
+
+type DeleteCDNClusterToApplicationParams struct {
+	ID           uint `uri:"id" binding:"required"`
+	CDNClusterID uint `uri:"cdn_cluster_id" binding:"required"`
+}
+
+type CreateApplicationRequest struct {
+	Name              string `json:"name" binding:"required"`
+	DownloadRateLimit string `json:"download_rate_limit" binding:"omitempty"`
+	URL               string `json:"url" binding:"omitempty"`
+	BIO               string `json:"bio" binding:"omitempty"`
+	UserID            uint   `json:"user_id" binding:"omitempty"`
+	State             string `json:"state" binding:"required,oneof=enable disable"`
+}
+
+type UpdateApplicationRequest struct {
+	Name              string `json:"name" binding:"required"`
+	DownloadRateLimit string `json:"download_rate_limit" binding:"required"`
+	URL               string `json:"url" binding:"required"`
+	State             string `json:"state" binding:"required,oneof=enable disable"`
+	BIO               string `json:"bio" binding:"omitempty"`
+	UserID            uint   `json:"user_id" binding:"omitempty"`
+}
+
+type GetApplicationsQuery struct {
+	Page    int `form:"page" binding:"omitempty,gte=1"`
+	PerPage int `form:"per_page" binding:"omitempty,gte=1,lte=50"`
+}
diff --git a/manager/types/cdn_cluster.go b/manager/types/cdn_cluster.go
index 33c9379b..07ce8e6e 100644
--- a/manager/types/cdn_cluster.go
+++ b/manager/types/cdn_cluster.go
@@ -31,17 +31,15 @@ type AddSchedulerClusterToCDNClusterParams struct {
 }
 
 type CreateCDNClusterRequest struct {
-	Name                string            `json:"name" binding:"required"`
-	BIO                 string            `json:"bio" binding:"omitempty"`
-	Config              *CDNClusterConfig `json:"config" binding:"required"`
-	SecurityGroupDomain string            `json:"security_group_domain" binding:"omitempty"`
+	Name   string            `json:"name" binding:"required"`
+	BIO    string            `json:"bio" binding:"omitempty"`
+	Config *CDNClusterConfig `json:"config" binding:"required"`
 }
 
 type UpdateCDNClusterRequest struct {
-	Name                string            `json:"name" binding:"omitempty"`
-	BIO                 string            `json:"bio" binding:"omitempty"`
-	Config              *CDNClusterConfig `json:"config" binding:"omitempty"`
-	SecurityGroupDomain string            `json:"security_group_domain" binding:"omitempty"`
+	Name   string            `json:"name" binding:"omitempty"`
+	BIO    string            `json:"bio" binding:"omitempty"`
+	Config *CDNClusterConfig `json:"config" binding:"omitempty"`
 }
 
 type GetCDNClustersQuery struct {
diff --git a/manager/types/scheduler_cluster.go b/manager/types/scheduler_cluster.go
index 58da0b35..931222f4 100644
--- a/manager/types/scheduler_cluster.go
+++ b/manager/types/scheduler_cluster.go
@@ -26,25 +26,23 @@ type AddSchedulerToSchedulerClusterParams struct {
 }
 
 type CreateSchedulerClusterRequest struct {
-	Name                string                        `json:"name" binding:"required"`
-	BIO                 string                        `json:"bio" binding:"omitempty"`
-	Config              *SchedulerClusterConfig       `json:"config" binding:"required"`
-	ClientConfig        *SchedulerClusterClientConfig `json:"client_config" binding:"required"`
-	Scopes              *SchedulerClusterScopes       `json:"scopes" binding:"omitempty"`
-	IsDefault           bool                          `json:"is_default" binding:"omitempty"`
-	CDNClusterID        uint                          `json:"cdn_cluster_id" binding:"omitempty"`
-	SecurityGroupDomain string                        `json:"security_group_domain" binding:"omitempty"`
+	Name         string                        `json:"name" binding:"required"`
+	BIO          string                        `json:"bio" binding:"omitempty"`
+	Config       *SchedulerClusterConfig       `json:"config" binding:"required"`
+	ClientConfig *SchedulerClusterClientConfig `json:"client_config" binding:"required"`
+	Scopes       *SchedulerClusterScopes       `json:"scopes" binding:"omitempty"`
+	IsDefault    bool                          `json:"is_default" binding:"omitempty"`
+	CDNClusterID uint                          `json:"cdn_cluster_id" binding:"omitempty"`
 }
 
 type UpdateSchedulerClusterRequest struct {
-	Name                string                        `json:"name" binding:"omitempty"`
-	BIO                 string                        `json:"bio" binding:"omitempty"`
-	Config              *SchedulerClusterConfig       `json:"config" binding:"omitempty"`
-	ClientConfig        *SchedulerClusterClientConfig `json:"client_config" binding:"omitempty"`
-	Scopes              *SchedulerClusterScopes       `json:"scopes" binding:"omitempty"`
-	IsDefault           bool                          `json:"is_default" binding:"omitempty"`
-	CDNClusterID        uint                          `json:"cdn_cluster_id" binding:"omitempty"`
-	SecurityGroupDomain string                        `json:"security_group_domain" binding:"omitempty"`
+	Name         string                        `json:"name" binding:"omitempty"`
+	BIO          string                        `json:"bio" binding:"omitempty"`
+	Config       *SchedulerClusterConfig       `json:"config" binding:"omitempty"`
+	ClientConfig *SchedulerClusterClientConfig `json:"client_config" binding:"omitempty"`
+	Scopes       *SchedulerClusterScopes       `json:"scopes" binding:"omitempty"`
+	IsDefault    bool                          `json:"is_default" binding:"omitempty"`
+	CDNClusterID uint                          `json:"cdn_cluster_id" binding:"omitempty"`
 }
 
 type GetSchedulerClustersQuery struct {
diff --git a/manager/types/security_group.go b/manager/types/security_group.go
index e01c4e0d..4c9fdbcf 100644
--- a/manager/types/security_group.go
+++ b/manager/types/security_group.go
@@ -30,23 +30,23 @@ type AddCDNClusterToSecurityGroupParams struct {
 	CDNClusterID uint `uri:"cdn_cluster_id" binding:"required"`
 }
 
+type AddSecurityRuleToSecurityGroupParams struct {
+	ID             uint `uri:"id" binding:"required"`
+	SecurityRuleID uint `uri:"security_rule_id" binding:"required"`
+}
+
 type CreateSecurityGroupRequest struct {
-	Name        string `json:"name" binding:"required"`
-	BIO         string `json:"bio" binding:"omitempty"`
-	Domain      string `json:"domain" binding:"required"`
-	ProxyDomain string `json:"proxy_domain" binding:"omitempty"`
+	Name string `json:"name" binding:"required"`
+	BIO  string `json:"bio" binding:"omitempty"`
 }
 
 type UpdateSecurityGroupRequest struct {
-	Name        string `json:"name" binding:"omitempty"`
-	BIO         string `json:"bio" binding:"omitempty"`
-	Domain      string `json:"domain" binding:"omitempty"`
-	ProxyDomain string `json:"proxy_domain" binding:"omitempty"`
+	Name string `json:"name" binding:"omitempty"`
+	BIO  string `json:"bio" binding:"omitempty"`
 }
 
 type GetSecurityGroupsQuery struct {
+	Name    string `form:"name" binding:"omitempty"`
 	Page    int    `form:"page" binding:"omitempty,gte=1"`
 	PerPage int    `form:"per_page" binding:"omitempty,gte=1,lte=50"`
-	Name    string `form:"name" binding:"omitempty"`
-	Domain  string `form:"domain" binding:"omitempty"`
 }
diff --git a/manager/types/security_rule.go b/manager/types/security_rule.go
new file mode 100644
index 00000000..4351578f
--- /dev/null
+++ b/manager/types/security_rule.go
@@ -0,0 +1,43 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package types
+
+type SecurityRuleParams struct {
+	ID uint `uri:"id" binding:"required"`
+}
+
+type CreateSecurityRuleRequest struct {
+	Name        string `json:"name" binding:"required"`
+	BIO         string `json:"bio" binding:"omitempty"`
+	Domain      string `json:"domain" binding:"required"`
+	ProxyDomain string `json:"proxy_domain" binding:"omitempty"`
+}
+
+type UpdateSecurityRuleRequest struct {
+	Name        string `json:"name" binding:"omitempty"`
+	BIO         string `json:"bio" binding:"omitempty"`
+	Domain      string `json:"domain" binding:"omitempty"`
+	ProxyDomain string `json:"proxy_domain" binding:"omitempty"`
+}
+
+type GetSecurityRulesQuery struct {
+	Page        int    `form:"page" binding:"omitempty,gte=1"`
+	PerPage     int    `form:"per_page" binding:"omitempty,gte=1,lte=50"`
+	Name        string `form:"name" binding:"omitempty"`
+	Domain      string `form:"domain" binding:"omitempty"`
+	ProxyDomain string `form:"proxy_domain" binding:"omitempty"`
+}
diff --git a/pkg/util/mathutils/math_utils.go b/pkg/util/mathutils/math_utils.go
index 2839ec14..c2b0af56 100644
--- a/pkg/util/mathutils/math_utils.go
+++ b/pkg/util/mathutils/math_utils.go
@@ -18,9 +18,20 @@
 package mathutils
 
 import (
+	"math"
 	"strconv"
 )
 
+const float64EqualityThreshold = 1e-9
+
+func MaxInt(a, b int) int {
+	if a < b {
+		return a
+	}
+
+	return b
+}
+
 func MaxInt32(a, b int32) int32 {
 	if a > b {
 		return a
@@ -29,6 +40,14 @@ func MaxInt32(a, b int32) int32 {
 	return b
 }
 
+func MinInt(a, b int) int {
+	if a < b {
+		return a
+	}
+
+	return b
+}
+
 func MinInt32(a, b int32) int32 {
 	if a < b {
 		return a
@@ -68,3 +87,7 @@ func IsInteger(value string) bool {
 
 	return false
 }
+
+func EqualFloat64(a, b float64) bool {
+	return math.Abs(a-b) <= float64EqualityThreshold
+}
diff --git a/scheduler/config/config.go b/scheduler/config/config.go
index 2619f005..0ffdc7af 100644
--- a/scheduler/config/config.go
+++ b/scheduler/config/config.go
@@ -43,9 +43,7 @@ type Config struct {
 func New() *Config {
 	return &Config{
 		Scheduler: &SchedulerConfig{
-			ABTest:               false,
-			AEvaluator:           "",
-			BEvaluator:           "",
+			Algorithm:            "default",
 			WorkerNum:            runtime.GOMAXPROCS(0),
 			BackSourceCount:      3,
 			AccessWindow:         3 * time.Minute,
@@ -179,9 +177,7 @@ type DynConfig struct {
 }
 
 type SchedulerConfig struct {
-	ABTest          bool   `yaml:"abtest" mapstructure:"abtest"`
-	AEvaluator      string `yaml:"aevaluator" mapstructure:"aevaluator"`
-	BEvaluator      string `yaml:"bevaluator" mapstructure:"bevaluator"`
+	Algorithm       string `yaml:"algorithm" mapstructure:"algorithm"`
 	WorkerNum       int    `yaml:"workerNum" mapstructure:"workerNum"`
 	BackSourceCount int32  `yaml:"backSourceCount" mapstructure:"backSourceCount"`
 	// AccessWindow should less than CDN task expireTime
diff --git a/scheduler/core/evaluator/basic/basic_evaluator.go b/scheduler/core/evaluator/basic/basic_evaluator.go
deleted file mode 100644
index f48fae00..00000000
--- a/scheduler/core/evaluator/basic/basic_evaluator.go
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- *     Copyright 2020 The Dragonfly Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package basic
-
-import (
-	logger "d7y.io/dragonfly/v2/internal/dflog"
-	"d7y.io/dragonfly/v2/scheduler/config"
-	"d7y.io/dragonfly/v2/scheduler/core/evaluator"
-	"d7y.io/dragonfly/v2/scheduler/supervisor"
-)
-
-type baseEvaluator struct {
-	cfg *config.SchedulerConfig
-}
-
-func NewEvaluator(cfg *config.SchedulerConfig) evaluator.Evaluator {
-	eval := &baseEvaluator{cfg: cfg}
-	logger.Debugf("create basic evaluator successfully")
-	return eval
-}
-
-func (eval *baseEvaluator) NeedAdjustParent(peer *supervisor.Peer) bool {
-	if peer.Host.IsCDN {
-		return false
-	}
-
-	parent, ok := peer.GetParent()
-	if !ok && !peer.IsDone() {
-		logger.Debugf("peer %s need adjust parent because it has not parent and status is %s", peer.ID, peer.GetStatus())
-		return true
-	}
-
-	// TODO Check whether the parent node is in the blacklist
-	if ok && eval.IsBadNode(parent) {
-		logger.Debugf("peer %s need adjust parent because it current parent is bad", peer.ID)
-		return true
-	}
-
-	if ok && parent.IsLeave() {
-		logger.Debugf("peer %s need adjust parent because it current parent is status is leave", peer.ID)
-		return true
-	}
-
-	costs := peer.GetPieceCosts()
-	if len(costs) < 4 {
-		return false
-	}
-
-	avgCost, lastCost := getAvgAndLastCost(costs, 4)
-	// TODO adjust policy
-	if (avgCost * 20) < lastCost {
-		logger.Debugf("peer %s need adjust parent because it latest download cost is too time consuming", peer.ID)
-		return true
-	}
-
-	return false
-}
-
-func (eval *baseEvaluator) IsBadNode(peer *supervisor.Peer) bool {
-	if peer.IsBad() {
-		logger.Debugf("peer %s is bad because it's status is %s", peer.ID, peer.GetStatus())
-		return true
-	}
-
-	costs := peer.GetPieceCosts()
-	if len(costs) < 4 {
-		return false
-	}
-
-	avgCost, lastCost := getAvgAndLastCost(costs, 4)
-	if avgCost*40 < lastCost && !peer.Host.IsCDN {
-		logger.Debugf("peer %s is bad because recent pieces have taken too long to download avg[%d] last[%d]", peer.ID, avgCost, lastCost)
-		return true
-	}
-
-	return false
-}
-
-// Evaluate The bigger, the better
-func (eval *baseEvaluator) Evaluate(parent *supervisor.Peer, child *supervisor.Peer) float64 {
-	profits := getProfits(parent, child)
-
-	load := getHostLoad(parent.Host)
-
-	dist := getDistance(parent, child)
-
-	return profits * load * dist
-}
-
-func getAvgAndLastCost(list []int, splitPos int) (avgCost, lastCost int) {
-	length := len(list)
-	totalCost := 0
-	for i, cost := range list {
-		totalCost += cost
-		if length-i <= splitPos {
-			lastCost += cost
-		}
-	}
-	avgCost = totalCost / length
-	lastCost = lastCost / splitPos
-	return
-}
-
-// getProfits 0.0~unlimited larger and better
-func getProfits(dst *supervisor.Peer, src *supervisor.Peer) float64 {
-	diff := dst.TotalPieceCount.Load() - src.TotalPieceCount.Load()
-	depth := dst.GetTreeDepth()
-
-	return float64(int(diff+1)*src.GetTreeNodeCount()) / float64(depth*depth)
-}
-
-// getHostLoad 0.0~1.0 larger and better
-func getHostLoad(host *supervisor.Host) float64 {
-	return 1.0 - host.GetUploadLoadPercent()
-}
-
-// getDistance 0.0~1.0 larger and better
-func getDistance(dst *supervisor.Peer, src *supervisor.Peer) float64 {
-	hostDist := 40.0
-	if dst.Host == src.Host {
-		hostDist = 0.0
-	} else {
-		if src.Host.NetTopology != "" && dst.Host.NetTopology == src.Host.NetTopology {
-			hostDist = 10.0
-		} else if src.Host.IDC != "" && dst.Host.IDC == src.Host.IDC {
-			hostDist = 20.0
-		} else if dst.Host.SecurityDomain != src.Host.SecurityDomain {
-			hostDist = 80.0
-		}
-	}
-	return 1.0 - hostDist/80.0
-}
diff --git a/scheduler/core/evaluator/evaluator.go b/scheduler/core/evaluator/evaluator.go
index b61ba594..a99b0dc3 100644
--- a/scheduler/core/evaluator/evaluator.go
+++ b/scheduler/core/evaluator/evaluator.go
@@ -17,19 +17,23 @@
 package evaluator
 
 import (
-	"sort"
-	"strings"
-	"sync"
-
-	"d7y.io/dragonfly/v2/internal/idgen"
-	"d7y.io/dragonfly/v2/scheduler/config"
 	"d7y.io/dragonfly/v2/scheduler/supervisor"
 )
 
-type Evaluator interface {
+const (
+	// DefaultAlgorithm is a rule-based scheduling algorithm
+	DefaultAlgorithm = "default"
 
+	// MLAlgorithm is a machine learning scheduling algorithm
+	MLAlgorithm = "ml"
+
+	// PluginAlgorithm is a scheduling algorithm based on plugin extension
+	PluginAlgorithm = "plugin"
+)
+
+type Evaluator interface {
 	// Evaluate todo Normalization
-	Evaluate(parent *supervisor.Peer, child *supervisor.Peer) float64
+	Evaluate(parent *supervisor.Peer, child *supervisor.Peer, taskPieceCount int32) float64
 
 	// NeedAdjustParent determine whether the peer needs a new parent node
 	NeedAdjustParent(peer *supervisor.Peer) bool
@@ -38,170 +42,16 @@ type Evaluator interface {
 	IsBadNode(peer *supervisor.Peer) bool
 }
 
-type Factory struct {
-	lock                         sync.RWMutex
-	evaluators                   map[string]Evaluator
-	getEvaluatorFuncs            map[int]getEvaluatorFunc
-	getEvaluatorFuncPriorityList []getEvaluatorFunc
-	cache                        map[string]Evaluator
-	cacheClearFunc               sync.Once
-	abtest                       bool
-	aEvaluator                   string
-	bEvaluator                   string
-}
-
-var _ Evaluator = (*Factory)(nil)
-
-func (ef *Factory) Evaluate(dst *supervisor.Peer, src *supervisor.Peer) float64 {
-	return ef.get(dst.Task.ID).Evaluate(dst, src)
-}
-
-func (ef *Factory) NeedAdjustParent(peer *supervisor.Peer) bool {
-	return ef.get(peer.Task.ID).NeedAdjustParent(peer)
-}
-
-func (ef *Factory) IsBadNode(peer *supervisor.Peer) bool {
-	return ef.get(peer.Task.ID).IsBadNode(peer)
-}
-
-func NewEvaluatorFactory(cfg *config.SchedulerConfig) *Factory {
-	factory := &Factory{
-		evaluators:        make(map[string]Evaluator),
-		getEvaluatorFuncs: map[int]getEvaluatorFunc{},
-		cache:             map[string]Evaluator{},
-		abtest:            cfg.ABTest,
-		aEvaluator:        cfg.AEvaluator,
-		bEvaluator:        cfg.BEvaluator,
-	}
-	return factory
-}
-
-var (
-	m = make(map[string]Evaluator)
-)
-
-func Register(name string, evaluator Evaluator) {
-	m[strings.ToLower(name)] = evaluator
-}
-
-func Get(name string) Evaluator {
-	if eval, ok := m[strings.ToLower(name)]; ok {
-		return eval
-	}
-	return nil
-}
-
-type getEvaluatorFunc func(taskID string) (string, bool)
-
-func (ef *Factory) get(taskID string) Evaluator {
-	ef.lock.RLock()
-	evaluator, ok := ef.cache[taskID]
-	ef.lock.RUnlock()
-	if ok {
-		return evaluator
-	}
-
-	if ef.abtest {
-		name := ""
-		if strings.HasSuffix(taskID, idgen.TwinsBSuffix) {
-			if ef.bEvaluator != "" {
-				name = ef.bEvaluator
-			}
-		} else {
-			if ef.aEvaluator != "" {
-				name = ef.aEvaluator
-			}
+func New(algorithm string) Evaluator {
+	switch algorithm {
+	case PluginAlgorithm:
+		if plugin, err := LoadPlugin(); err == nil {
+			return plugin
 		}
-		if name != "" {
-			ef.lock.RLock()
-			evaluator, ok = ef.evaluators[name]
-			ef.lock.RUnlock()
-			if ok {
-				ef.lock.Lock()
-				ef.cache[taskID] = evaluator
-				ef.lock.Unlock()
-				return evaluator
-			}
-		}
-	}
-
-	for _, fun := range ef.getEvaluatorFuncPriorityList {
-		name, ok := fun(taskID)
-		if !ok {
-			continue
-		}
-		ef.lock.RLock()
-		evaluator, ok = ef.evaluators[name]
-		ef.lock.RUnlock()
-		if !ok {
-			continue
-		}
-
-		ef.lock.Lock()
-		ef.cache[taskID] = evaluator
-		ef.lock.Unlock()
-		return evaluator
-	}
-
-	return nil
-}
-
-func (ef *Factory) clearCache() {
-	ef.lock.Lock()
-	ef.cache = make(map[string]Evaluator)
-	ef.lock.Unlock()
-}
-
-func (ef *Factory) add(name string, evaluator Evaluator) {
-	ef.lock.Lock()
-	ef.evaluators[name] = evaluator
-	ef.lock.Unlock()
-}
-
-func (ef *Factory) addGetEvaluatorFunc(priority int, fun getEvaluatorFunc) {
-	ef.lock.Lock()
-	defer ef.lock.Unlock()
-	_, ok := ef.getEvaluatorFuncs[priority]
-	if ok {
-		return
+	// TODO Implement MLAlgorithm
+	case MLAlgorithm, DefaultAlgorithm:
+		return NewEvaluatorBase()
 	}
-	ef.getEvaluatorFuncs[priority] = fun
-	var priorities []int
-	for p := range ef.getEvaluatorFuncs {
-		priorities = append(priorities, p)
-	}
-	sort.Ints(priorities)
-	ef.getEvaluatorFuncPriorityList = ef.getEvaluatorFuncPriorityList[:0]
-	for i := len(priorities) - 1; i >= 0; i-- {
-		ef.getEvaluatorFuncPriorityList = append(ef.getEvaluatorFuncPriorityList, ef.getEvaluatorFuncs[priorities[i]])
-	}
-
-}
-
-func (ef *Factory) deleteGetEvaluatorFunc(priority int, fun getEvaluatorFunc) {
-	ef.lock.Lock()
-
-	delete(ef.getEvaluatorFuncs, priority)
-
-	var priorities []int
-	for p := range ef.getEvaluatorFuncs {
-		priorities = append(priorities, p)
-	}
-	sort.Ints(priorities)
-	ef.getEvaluatorFuncPriorityList = ef.getEvaluatorFuncPriorityList[:0]
-	for i := len(priorities) - 1; i >= 0; i-- {
-		ef.getEvaluatorFuncPriorityList = append(ef.getEvaluatorFuncPriorityList, ef.getEvaluatorFuncs[priorities[i]])
-	}
-
-	ef.lock.Unlock()
-}
-
-func (ef *Factory) Register(name string, evaluator Evaluator) {
-	ef.add(name, evaluator)
-	ef.clearCache()
-}
 
-func (ef *Factory) RegisterGetEvaluatorFunc(priority int, fun getEvaluatorFunc) {
-	ef.addGetEvaluatorFunc(priority, fun)
-	ef.clearCache()
+	return NewEvaluatorBase()
 }
diff --git a/scheduler/core/evaluator/evaluator_base.go b/scheduler/core/evaluator/evaluator_base.go
new file mode 100644
index 00000000..66356ec9
--- /dev/null
+++ b/scheduler/core/evaluator/evaluator_base.go
@@ -0,0 +1,233 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package evaluator
+
+import (
+	"math/big"
+	"strings"
+
+	"github.com/montanaflynn/stats"
+
+	logger "d7y.io/dragonfly/v2/internal/dflog"
+	"d7y.io/dragonfly/v2/pkg/util/mathutils"
+	"d7y.io/dragonfly/v2/scheduler/supervisor"
+)
+
+const (
+	// Finished piece weight
+	finishedPieceWeight float64 = 0.4
+
+	// Free load weight
+	freeLoadWeight = 0.3
+
+	// IDC affinity weight
+	idcAffinityWeight = 0.15
+
+	// NetTopology affinity weight
+	netTopologyAffinityWeight = 0.1
+
+	// Location affinity weight
+	locationAffinityWeight = 0.05
+)
+
+const (
+	// Maximum score
+	maxScore float64 = 1
+
+	// Minimum score
+	minScore = 0
+)
+
+const (
+	// If the number of samples is greater than or equal to 10,
+	// it is close to the normal distribution
+	normalDistributionLen = 10
+
+	// When costs len is greater than or equal to 2,
+	// the last cost can be compared and calculated
+	minAvailableCostLen = 2
+
+	// Maximum number of elements
+	maxElementLen = 5
+)
+
+type evaluatorBase struct{}
+
+func NewEvaluatorBase() Evaluator {
+	return &evaluatorBase{}
+}
+
+// The larger the value after evaluation, the higher the priority
+func (eb *evaluatorBase) Evaluate(parent *supervisor.Peer, child *supervisor.Peer, taskPieceCount int32) float64 {
+	// If the SecurityDomain of hosts exists but is not equal,
+	// it cannot be scheduled as a parent
+	if parent.Host.SecurityDomain != "" &&
+		child.Host.SecurityDomain != "" &&
+		strings.Compare(parent.Host.SecurityDomain, child.Host.SecurityDomain) != 0 {
+		return minScore
+	}
+
+	return finishedPieceWeight*calculatePieceScore(parent, child, taskPieceCount) +
+		freeLoadWeight*calculateFreeLoadScore(parent.Host) +
+		idcAffinityWeight*calculateIDCAffinityScore(parent.Host, child.Host) +
+		netTopologyAffinityWeight*calculateMultiElementAffinityScore(parent.Host.NetTopology, child.Host.NetTopology) +
+		locationAffinityWeight*calculateMultiElementAffinityScore(parent.Host.Location, child.Host.Location)
+}
+
+// calculatePieceScore 0.0~unlimited larger and better
+func calculatePieceScore(parent *supervisor.Peer, child *supervisor.Peer, taskPieceCount int32) float64 {
+	// If the total piece is determined, normalize the number of
+	// pieces downloaded by the parent node
+	if taskPieceCount > 0 {
+		finishedPieceCount := parent.TotalPieceCount.Load()
+		return float64(finishedPieceCount) / float64(taskPieceCount)
+	}
+
+	// Use the difference between the parent node and the child node to
+	// download the piece to roughly represent the piece score
+	parentFinishedPieceCount := parent.TotalPieceCount.Load()
+	childFinishedPieceCount := child.TotalPieceCount.Load()
+	return float64(parentFinishedPieceCount - childFinishedPieceCount)
+}
+
+// calculateFreeLoadScore 0.0~1.0 larger and better
+func calculateFreeLoadScore(host *supervisor.Host) float64 {
+	load := host.CurrentUploadLoad.Load()
+	totalLoad := host.TotalUploadLoad
+	return float64(totalLoad-load) / float64(totalLoad)
+}
+
+// calculateIDCAffinityScore 0.0~1.0 larger and better
+func calculateIDCAffinityScore(dst, src *supervisor.Host) float64 {
+	if dst.IDC != "" && src.IDC != "" && strings.Compare(dst.IDC, src.IDC) == 0 {
+		return maxScore
+	}
+
+	return minScore
+}
+
+// calculateMultiElementAffinityScore 0.0~1.0 larger and better
+func calculateMultiElementAffinityScore(dst, src string) float64 {
+	if dst == "" || src == "" {
+		return minScore
+	}
+
+	if strings.Compare(dst, src) == 0 {
+		return maxScore
+	}
+
+	// Calculate the number of multi-element matches divided by "|"
+	var score, elementLen int
+	dstElements := strings.Split(dst, "|")
+	srcElements := strings.Split(src, "|")
+	elementLen = mathutils.MaxInt(len(dstElements), len(srcElements))
+
+	// Maximum element length is 5
+	if elementLen > maxElementLen {
+		elementLen = maxElementLen
+	}
+
+	for i := 0; i < elementLen; i++ {
+		if strings.Compare(dstElements[i], srcElements[i]) != 0 {
+			break
+		}
+		score++
+	}
+
+	return float64(score) / float64(maxElementLen)
+}
+
+func (eb *evaluatorBase) NeedAdjustParent(peer *supervisor.Peer) bool {
+	// CDN is the root node
+	if peer.Host.IsCDN {
+		return false
+	}
+
+	parent, ok := peer.GetParent()
+	// Peer has no parent and is not completed
+	if !ok && !peer.IsDone() {
+		logger.Infof("peer %s need adjust parent because it has not parent and status is %s", peer.ID, peer.GetStatus())
+		return true
+	}
+
+	// Peer has parent but parent can't be scheduled.
+	if ok && (parent.IsLeave() || eb.IsBadNode(parent)) {
+		logger.Infof("peer %s need adjust parent because parent can't be scheduled", peer.ID)
+		return true
+	}
+
+	// Determine whether to adjust parent based on piece download costs
+	rawCosts := peer.GetPieceCosts()
+	costs := stats.LoadRawData(rawCosts)
+	len := len(costs)
+	// Peer has not finished downloading enough piece
+	if len < minAvailableCostLen {
+		logger.Infof("peer %s has not finished downloading enough piece, it can't be adjusted parent", peer.ID)
+		return false
+	}
+
+	lastCost := costs[len-1]
+	mean, _ := stats.Mean(costs[:len-1]) // nolint: errcheck
+
+	// Download costs does not meet the normal distribution,
+	// if the last cost is five times more than mean, it need to be adjusted parent.
+	if len < normalDistributionLen {
+		isNeedAdjustParent := big.NewFloat(lastCost).Cmp(big.NewFloat(mean*5)) > 0
+		logger.Infof("peer %s does not meet the normal distribution and mean is %.2f, peer need adjust parent: %t", peer.ID, mean, isNeedAdjustParent)
+		return isNeedAdjustParent
+	}
+
+	// Download costs satisfies the normal distribution,
+	// last cost falling outside of three-sigma effect need to be adjusted parent,
+	// refer to https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule
+	stdev, _ := stats.StandardDeviation(costs[:len-2]) // nolint: errcheck
+	isNeedAdjustParent := big.NewFloat(lastCost).Cmp(big.NewFloat(mean+3*stdev)) > 0
+	logger.Infof("peer %s meet the normal distribution, costs mean is %.2f and standard deviation is %.2f, peer need adjust parent: %t",
+		peer.ID, mean, stdev, isNeedAdjustParent)
+	return isNeedAdjustParent
+}
+
+func (eb *evaluatorBase) IsBadNode(peer *supervisor.Peer) bool {
+	if peer.IsBad() {
+		logger.Infof("peer %s is bad because it's status is %s", peer.ID, peer.GetStatus())
+		return true
+	}
+
+	if peer.Host.IsCDN {
+		logger.Infof("peer %s is cdn can't be bad node", peer.ID)
+		return false
+	}
+
+	// Determine whether to bad node based on piece download costs
+	rawCosts := peer.GetPieceCosts()
+	costs := stats.LoadRawData(rawCosts)
+	len := len(costs)
+	// Peer has not finished downloading enough piece
+	if len < minAvailableCostLen {
+		logger.Infof("peer %s has not finished downloading enough piece, it can't be bad node", peer.ID)
+		return false
+	}
+
+	lastCost := costs[len-1]
+	mean, _ := stats.Mean(costs[:len-1]) // nolint: errcheck
+
+	// Download costs does not meet the normal distribution,
+	// if the last cost is forty times more than mean, it is bad node.
+	isBadNode := big.NewFloat(lastCost).Cmp(big.NewFloat(mean*40)) > 0
+	logger.Infof("peer %s mean is %.2f and it is bad node: %t", peer.ID, mean, isBadNode)
+	return isBadNode
+}
diff --git a/scheduler/core/evaluator/plugin.go b/scheduler/core/evaluator/plugin.go
new file mode 100644
index 00000000..41a6caa9
--- /dev/null
+++ b/scheduler/core/evaluator/plugin.go
@@ -0,0 +1,39 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package evaluator
+
+import (
+	"errors"
+
+	"d7y.io/dragonfly/v2/internal/dfplugin"
+)
+
+const (
+	pluginName = "evaluator"
+)
+
+func LoadPlugin() (Evaluator, error) {
+	client, _, err := dfplugin.Load(dfplugin.PluginTypeScheduler, pluginName, map[string]string{})
+	if err != nil {
+		return nil, err
+	}
+
+	if rc, ok := client.(Evaluator); ok {
+		return rc, err
+	}
+	return nil, errors.New("invalid client, not a ResourceClient")
+}
diff --git a/scheduler/core/scheduler/basic/basic_scheduler.go b/scheduler/core/scheduler/basic/basic_scheduler.go
index 05b8bc49..dd7fedf1 100644
--- a/scheduler/core/scheduler/basic/basic_scheduler.go
+++ b/scheduler/core/scheduler/basic/basic_scheduler.go
@@ -24,7 +24,6 @@ import (
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 	"d7y.io/dragonfly/v2/scheduler/config"
 	"d7y.io/dragonfly/v2/scheduler/core/evaluator"
-	"d7y.io/dragonfly/v2/scheduler/core/evaluator/basic"
 	"d7y.io/dragonfly/v2/scheduler/core/scheduler"
 	"d7y.io/dragonfly/v2/scheduler/supervisor"
 )
@@ -45,15 +44,11 @@ func newBasicSchedulerBuilder() scheduler.Builder {
 	}
 }
 
-var _ scheduler.Builder = (*basicSchedulerBuilder)(nil)
-
 func (builder *basicSchedulerBuilder) Build(cfg *config.SchedulerConfig, opts *scheduler.BuildOptions) (scheduler.Scheduler, error) {
 	logger.Debugf("start create basic scheduler...")
-	evalFactory := evaluator.NewEvaluatorFactory(cfg)
-	evalFactory.Register("default", basic.NewEvaluator(cfg))
-	evalFactory.RegisterGetEvaluatorFunc(0, func(taskID string) (string, bool) { return "default", true })
+	evaluator := evaluator.New(cfg.Algorithm)
 	sched := &Scheduler{
-		evaluator:   evalFactory,
+		evaluator:   evaluator,
 		peerManager: opts.PeerManager,
 		cfg:         cfg,
 	}
@@ -83,8 +78,9 @@ func (s *Scheduler) ScheduleChildren(peer *supervisor.Peer, blankChildren sets.S
 	}
 	evalResult := make(map[float64][]*supervisor.Peer)
 	var evalScore []float64
+	taskTotalPieceCount := peer.Task.TotalPieceCount.Load()
 	for _, child := range candidateChildren {
-		score := s.evaluator.Evaluate(peer, child)
+		score := s.evaluator.Evaluate(peer, child, taskTotalPieceCount)
 		evalResult[score] = append(evalResult[score], child)
 		evalScore = append(evalScore, score)
 	}
@@ -119,8 +115,9 @@ func (s *Scheduler) ScheduleParent(peer *supervisor.Peer, blankParents sets.Stri
 	}
 	evalResult := make(map[float64][]*supervisor.Peer)
 	var evalScore []float64
+	taskTotalPieceCount := peer.Task.TotalPieceCount.Load()
 	for _, parent := range candidateParents {
-		score := s.evaluator.Evaluate(parent, peer)
+		score := s.evaluator.Evaluate(parent, peer, taskTotalPieceCount)
 		peer.Log().Debugf("evaluate score candidate %s is %f", parent.ID, score)
 		evalResult[score] = append(evalResult[score], parent)
 		evalScore = append(evalScore, score)
diff --git a/scheduler/core/service.go b/scheduler/core/service.go
index cc73a352..7b92e154 100644
--- a/scheduler/core/service.go
+++ b/scheduler/core/service.go
@@ -31,9 +31,7 @@ import (
 	"d7y.io/dragonfly/v2/internal/dfcodes"
 	"d7y.io/dragonfly/v2/internal/dferrors"
 	logger "d7y.io/dragonfly/v2/internal/dflog"
-	"d7y.io/dragonfly/v2/internal/idgen"
 	"d7y.io/dragonfly/v2/pkg/gc"
-	"d7y.io/dragonfly/v2/pkg/rpc/base"
 	"d7y.io/dragonfly/v2/pkg/rpc/base/common"
 	schedulerRPC "d7y.io/dragonfly/v2/pkg/rpc/scheduler"
 	"d7y.io/dragonfly/v2/pkg/synclock"
@@ -208,13 +206,6 @@ func (s *SchedulerService) Stop() {
 	s.wg.Wait()
 }
 
-func (s *SchedulerService) GenerateTaskID(url string, meta *base.UrlMeta, peerID string) string {
-	if s.config.ABTest {
-		return idgen.TwinsTaskID(url, meta, peerID)
-	}
-	return idgen.TaskID(url, meta)
-}
-
 func (s *SchedulerService) SelectParent(peer *supervisor.Peer) (parent *supervisor.Peer, err error) {
 	parent, _, hasParent := s.sched.ScheduleParent(peer, sets.NewString())
 	if !hasParent || parent == nil {
diff --git a/scheduler/rpcserver/rpcserver.go b/scheduler/rpcserver/rpcserver.go
index b288a648..35d5033b 100644
--- a/scheduler/rpcserver/rpcserver.go
+++ b/scheduler/rpcserver/rpcserver.go
@@ -28,6 +28,7 @@ import (
 	"d7y.io/dragonfly/v2/internal/dfcodes"
 	"d7y.io/dragonfly/v2/internal/dferrors"
 	logger "d7y.io/dragonfly/v2/internal/dflog"
+	"d7y.io/dragonfly/v2/internal/idgen"
 	"d7y.io/dragonfly/v2/pkg/rpc/base"
 	"d7y.io/dragonfly/v2/pkg/rpc/scheduler"
 	schedulerserver "d7y.io/dragonfly/v2/pkg/rpc/scheduler/server"
@@ -71,7 +72,8 @@ func (s *server) RegisterPeerTask(ctx context.Context, request *scheduler.PeerTa
 		span.RecordError(err)
 		return
 	}
-	taskID := s.service.GenerateTaskID(request.Url, request.UrlMeta, request.PeerId)
+
+	taskID := idgen.TaskID(request.Url, request.UrlMeta)
 	span.SetAttributes(config.AttributeTaskID.String(taskID))
 	task := s.service.GetOrCreateTask(ctx, supervisor.NewTask(taskID, request.Url, request.UrlMeta))
 	if task.IsFail() {
diff --git a/scheduler/supervisor/host.go b/scheduler/supervisor/host.go
index 9d88044a..bbc568a7 100644
--- a/scheduler/supervisor/host.go
+++ b/scheduler/supervisor/host.go
@@ -174,14 +174,6 @@ func (h *Host) GetPeersLen() int {
 	return length
 }
 
-func (h *Host) GetUploadLoadPercent() float64 {
-	if h.TotalUploadLoad <= 0 {
-		return 1.0
-	}
-
-	return float64(h.CurrentUploadLoad.Load()) / float64(h.TotalUploadLoad)
-}
-
 func (h *Host) GetFreeUploadLoad() int32 {
 	return h.TotalUploadLoad - h.CurrentUploadLoad.Load()
 }
diff --git a/scheduler/supervisor/peer.go b/scheduler/supervisor/peer.go
index 45dd6dec..ea616039 100644
--- a/scheduler/supervisor/peer.go
+++ b/scheduler/supervisor/peer.go
@@ -368,35 +368,20 @@ func (peer *Peer) GetPieceCosts() []int {
 	return peer.pieceCosts
 }
 
-func (peer *Peer) GetPieceAverageCost() (int, bool) {
-	costs := peer.GetPieceCosts()
-	if len(costs) < 1 {
-		return 0, false
-	}
-
-	totalCost := 0
-	for _, cost := range costs {
-		totalCost += cost
-	}
+func (peer *Peer) SetPieceCosts(costs ...int) {
+	peer.lock.Lock()
+	defer peer.lock.Unlock()
 
-	return totalCost / len(costs), true
+	peer.pieceCosts = append(peer.pieceCosts, costs...)
 }
 
 func (peer *Peer) UpdateProgress(finishedCount int32, cost int) {
 	if finishedCount > peer.TotalPieceCount.Load() {
 		peer.TotalPieceCount.Store(finishedCount)
-
-		peer.lock.Lock()
-		peer.pieceCosts = append(peer.pieceCosts, cost)
-		if len(peer.pieceCosts) > 20 {
-			peer.pieceCosts = peer.pieceCosts[len(peer.pieceCosts)-20:]
-		}
-		peer.lock.Unlock()
-
+		peer.SetPieceCosts(cost)
 		peer.Task.UpdatePeer(peer)
 		return
 	}
-
 }
 
 func (peer *Peer) SortedValue() int {
@@ -404,8 +389,12 @@ func (peer *Peer) SortedValue() int {
 	defer peer.lock.RUnlock()
 
 	pieceCount := peer.TotalPieceCount.Load()
-	hostLoad := peer.getFreeLoad()
-	return int(pieceCount*HostMaxLoad + hostLoad)
+	freeLoad := peer.getFreeLoad()
+	if peer.Host.IsCDN {
+		// if peer's host is CDN, peer has the lowest priority among all peers with the same number of pieces
+		return int(pieceCount * HostMaxLoad)
+	}
+	return int(pieceCount*HostMaxLoad + freeLoad)
 }
 
 func (peer *Peer) getFreeLoad() int32 {


Test patch:
diff --git a/internal/idgen/task_id_test.go b/internal/idgen/task_id_test.go
index 88b52be4..cd44652d 100644
--- a/internal/idgen/task_id_test.go
+++ b/internal/idgen/task_id_test.go
@@ -85,78 +85,3 @@ func TestTaskID(t *testing.T) {
 		})
 	}
 }
-
-func TestTwinsTaskID(t *testing.T) {
-	tests := []struct {
-		name   string
-		url    string
-		meta   *base.UrlMeta
-		peerID string
-		expect func(t *testing.T, d interface{})
-	}{
-		{
-			name:   "generate taskID with url",
-			url:    "https://example.com",
-			meta:   &base.UrlMeta{},
-			peerID: "foo",
-			expect: func(t *testing.T, d interface{}) {
-				assert := assert.New(t)
-				assert.Equal("100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9_B", d)
-			},
-		},
-		{
-			name: "generate taskID with meta",
-			url:  "https://example.com",
-			meta: &base.UrlMeta{
-				Range:  "foo",
-				Digest: "bar",
-			},
-			peerID: "foo",
-			expect: func(t *testing.T, d interface{}) {
-				assert := assert.New(t)
-				assert.Equal("aeee0e0a2a0c75130582641353c539aaf9011a0088b31347f7588e70e449a3e0_B", d)
-			},
-		},
-		{
-			name: "generate taskID with filter",
-			url:  "https://example.com?foo=foo&bar=bar",
-			meta: &base.UrlMeta{
-				Filter: "foo&bar",
-			},
-			peerID: "foo",
-			expect: func(t *testing.T, d interface{}) {
-				assert := assert.New(t)
-				assert.Equal("100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9_B", d)
-			},
-		},
-		{
-			name: "generate taskID with tag",
-			url:  "https://example.com",
-			meta: &base.UrlMeta{
-				Tag: "foo",
-			},
-			peerID: "foo",
-			expect: func(t *testing.T, d interface{}) {
-				assert := assert.New(t)
-				assert.Equal("2773851c628744fb7933003195db436ce397c1722920696c4274ff804d86920b_B", d)
-			},
-		},
-		{
-			name:   "generate twinsA taskID",
-			url:    "https://example.com",
-			meta:   &base.UrlMeta{},
-			peerID: "bar",
-			expect: func(t *testing.T, d interface{}) {
-				assert := assert.New(t)
-				assert.Equal("100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9_A", d)
-			},
-		},
-	}
-
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			data := TwinsTaskID(tc.url, tc.meta, tc.peerID)
-			tc.expect(t, data)
-		})
-	}
-}
diff --git a/manager/searcher/searcher_test.go b/manager/searcher/searcher_test.go
index a4c9e070..b9c67e43 100644
--- a/manager/searcher/searcher_test.go
+++ b/manager/searcher/searcher_test.go
@@ -55,7 +55,17 @@ func TestSchedulerCluster(t *testing.T) {
 				{
 					Name: "foo",
 					SecurityGroup: model.SecurityGroup{
-						Domain: "domain-1",
+						SecurityRules: []model.SecurityRule{
+							{
+								Domain: "domain-1",
+							},
+						},
+					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
 					},
 				},
 				{
@@ -77,9 +87,21 @@ func TestSchedulerCluster(t *testing.T) {
 					Scopes: map[string]interface{}{
 						"location": []string{"location-1"},
 					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
+					},
 				},
 				{
 					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
 				},
 			},
 			conditions: map[string]string{"location": "location-1"},
@@ -97,9 +119,21 @@ func TestSchedulerCluster(t *testing.T) {
 					Scopes: map[string]interface{}{
 						"idc": []string{"idc-1"},
 					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
+					},
 				},
 				{
 					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
 				},
 			},
 			conditions: map[string]string{"idc": "idc-1"},
@@ -109,6 +143,38 @@ func TestSchedulerCluster(t *testing.T) {
 				assert.Equal(ok, true)
 			},
 		},
+		{
+			name: "match according to net topology condition",
+			schedulerClusters: []model.SchedulerCluster{
+				{
+					Name: "foo",
+					Scopes: map[string]interface{}{
+						"net_topology": []string{"net-topology-1"},
+					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
+					},
+				},
+				{
+					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
+				},
+			},
+			conditions: map[string]string{"net_topology": "net-topology-1"},
+			expect: func(t *testing.T, data model.SchedulerCluster, ok bool) {
+				assert := assert.New(t)
+				assert.Equal(data.Name, "foo")
+				assert.Equal(ok, true)
+			},
+		},
 		{
 			name: "match according to location and idc condition",
 			schedulerClusters: []model.SchedulerCluster{
@@ -118,9 +184,21 @@ func TestSchedulerCluster(t *testing.T) {
 						"location": []string{"location-1"},
 						"idc":      []string{"idc-1"},
 					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
+					},
 				},
 				{
 					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
 				},
 			},
 			conditions: map[string]string{
@@ -142,11 +220,27 @@ func TestSchedulerCluster(t *testing.T) {
 						"location": []string{"location-1"},
 					},
 					SecurityGroup: model.SecurityGroup{
-						Domain: "domain-1",
+						SecurityRules: []model.SecurityRule{
+							{
+								Domain: "domain-1",
+							},
+						},
+					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
 					},
 				},
 				{
 					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
 				},
 			},
 			conditions: map[string]string{
@@ -168,11 +262,27 @@ func TestSchedulerCluster(t *testing.T) {
 						"idc": []string{"idc-1"},
 					},
 					SecurityGroup: model.SecurityGroup{
-						Domain: "domain-1",
+						SecurityRules: []model.SecurityRule{
+							{
+								Domain: "domain-1",
+							},
+						},
+					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
 					},
 				},
 				{
 					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
 				},
 			},
 			conditions: map[string]string{
@@ -195,11 +305,27 @@ func TestSchedulerCluster(t *testing.T) {
 						"location": []string{"location-1"},
 					},
 					SecurityGroup: model.SecurityGroup{
-						Domain: "domain-1",
+						SecurityRules: []model.SecurityRule{
+							{
+								Domain: "domain-1",
+							},
+						},
+					},
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "foo",
+							Status:   "active",
+						},
 					},
 				},
 				{
 					Name: "bar",
+					Schedulers: []model.Scheduler{
+						{
+							HostName: "bar",
+							Status:   "active",
+						},
+					},
 				},
 			},
 			conditions: map[string]string{
@@ -223,127 +349,3 @@ func TestSchedulerCluster(t *testing.T) {
 		})
 	}
 }
-
-func TestCalculateSchedulerClusterMean(t *testing.T) {
-	tests := []struct {
-		name       string
-		conditions map[string]string
-		rawScopes  map[string]interface{}
-		expect     func(t *testing.T, mean float64)
-	}{
-		{
-			name:       "conditions and rawScopes is empty",
-			conditions: map[string]string{},
-			rawScopes:  map[string]interface{}{},
-			expect: func(t *testing.T, mean float64) {
-				assert := assert.New(t)
-				assert.Equal(mean, float64(0))
-			},
-		},
-		{
-			name: "missed matches",
-			conditions: map[string]string{
-				"location": "location-1",
-			},
-			rawScopes: map[string]interface{}{
-				"idc": []string{"idc-1"},
-			},
-			expect: func(t *testing.T, mean float64) {
-				assert := assert.New(t)
-				assert.Equal(mean, float64(0))
-			},
-		},
-		{
-			name: "match according to location",
-			conditions: map[string]string{
-				"location": "location-1",
-			},
-			rawScopes: map[string]interface{}{
-				"location": []string{"location-1"},
-			},
-			expect: func(t *testing.T, mean float64) {
-				assert := assert.New(t)
-				assert.Equal(mean, float64(conditionLocationWeight))
-			},
-		},
-		{
-			name: "match according to idc",
-			conditions: map[string]string{
-				"idc": "idc-1",
-			},
-			rawScopes: map[string]interface{}{
-				"idc": []string{"idc-1"},
-			},
-			expect: func(t *testing.T, mean float64) {
-				assert := assert.New(t)
-				assert.Equal(mean, float64(conditionIDCWeight))
-			},
-		},
-		{
-			name: "match according to location and idc",
-			conditions: map[string]string{
-				"location": "location-1",
-				"idc":      "idc-1",
-			},
-			rawScopes: map[string]interface{}{
-				"location": []string{"location-1"},
-				"idc":      []string{"idc-1"},
-			},
-			expect: func(t *testing.T, mean float64) {
-				assert := assert.New(t)
-				assert.Equal(mean, float64(conditionLocationWeight+conditionIDCWeight))
-			},
-		},
-	}
-
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			mean := calculateSchedulerClusterMean(tc.conditions, tc.rawScopes)
-			tc.expect(t, mean)
-		})
-	}
-}
-
-func TestCalculateConditionScore(t *testing.T) {
-	tests := []struct {
-		name   string
-		value  string
-		scope  []string
-		expect func(t *testing.T, score float64)
-	}{
-		{
-			name:  "value is empty",
-			value: "",
-			scope: []string{"foo"},
-			expect: func(t *testing.T, score float64) {
-				assert := assert.New(t)
-				assert.Equal(score, float64(0))
-			},
-		},
-		{
-			name:  "scope is empty",
-			value: "foo",
-			scope: []string{},
-			expect: func(t *testing.T, score float64) {
-				assert := assert.New(t)
-				assert.Equal(score, float64(0))
-			},
-		},
-		{
-			name:  "match according to value",
-			value: "foo",
-			scope: []string{"foo"},
-			expect: func(t *testing.T, score float64) {
-				assert := assert.New(t)
-				assert.Equal(score, float64(1))
-			},
-		},
-	}
-
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			score := calculateConditionScore(tc.value, tc.scope)
-			tc.expect(t, score)
-		})
-	}
-}
diff --git a/scheduler/config/config_test.go b/scheduler/config/config_test.go
index 4108821c..1f406083 100644
--- a/scheduler/config/config_test.go
+++ b/scheduler/config/config_test.go
@@ -39,10 +39,8 @@ func TestSchedulerConfig_Load(t *testing.T) {
 			CDNDirPath: "tmp",
 		},
 		Scheduler: &SchedulerConfig{
-			ABTest:     true,
-			AEvaluator: "a-evaluator",
-			BEvaluator: "b-evaluator",
-			WorkerNum:  8,
+			Algorithm: "default",
+			WorkerNum: 8,
 		},
 		Server: &ServerConfig{
 			IP:   "127.0.0.1",
diff --git a/scheduler/config/testdata/scheduler.yaml b/scheduler/config/testdata/scheduler.yaml
index 7d1268af..5c15aa66 100644
--- a/scheduler/config/testdata/scheduler.yaml
+++ b/scheduler/config/testdata/scheduler.yaml
@@ -4,13 +4,11 @@ dynconfig:
   cdnDirPath: tmp
 
 scheduler:
-  abtest: true
-  aevaluator: "a-evaluator"
-  bevaluator: "b-evaluator"
   workerNum: 8
   workerJobPoolSize: 10000
   senderNum: 10
   senderJobPoolSize: 10000
+  algorithm: default
 
 server:
   ip: "127.0.0.1"
diff --git a/scheduler/core/evaluator/evaluator_base_test.go b/scheduler/core/evaluator/evaluator_base_test.go
new file mode 100644
index 00000000..dd26231c
--- /dev/null
+++ b/scheduler/core/evaluator/evaluator_base_test.go
@@ -0,0 +1,667 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package evaluator
+
+import (
+	"testing"
+
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/assert"
+
+	"d7y.io/dragonfly/v2/internal/idgen"
+	"d7y.io/dragonfly/v2/pkg/util/mathutils"
+	"d7y.io/dragonfly/v2/scheduler/supervisor"
+)
+
+const (
+	cdnHostType    = "cdn"
+	clientHostType = "client"
+)
+
+const (
+	mockIP      = "127.0.0.1"
+	mockTaskURL = "https://example.com"
+)
+
+type factor struct {
+	hostType           string
+	securityDomain     string
+	idc                string
+	location           string
+	netTopology        string
+	totalUploadLoad    int32
+	currentUploadLoad  int32
+	finishedPieceCount int32
+	hostUUID           string
+	taskPieceCount     int32
+}
+
+func TestEvaluatorEvaluate(t *testing.T) {
+	tests := []struct {
+		name   string
+		parent *factor
+		child  *factor
+		expect func(t *testing.T, v float64)
+	}{
+		{
+			name: "evaluate succeeded with cdn peer",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.6)))
+			},
+		},
+		{
+			name: "evaluate with different securityDomain",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foz",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0)))
+			},
+		},
+		{
+			name: "evaluate with empty securityDomain",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.6)))
+			},
+		},
+		{
+			name: "evaluate with different idc",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "baz",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.45)))
+			},
+		},
+		{
+			name: "evaluate with different location",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.59)))
+			},
+		},
+		{
+			name: "evaluate with empty location",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.55)))
+			},
+		},
+		{
+			name: "evaluate with excessive location",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e|f",
+				netTopology:        "a|b|c|d|e",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e|f",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.6)))
+			},
+		},
+		{
+			name: "evaluate with different netTopology",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.58)))
+			},
+		},
+		{
+			name: "evaluate with empty netTopology",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.5)))
+			},
+		},
+		{
+			name: "evaluate with excessive netTopology",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e|f",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e|f",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.6)))
+			},
+		},
+		{
+			name: "evaluate with task piece count",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e|f",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 20,
+				hostUUID:           "example",
+				taskPieceCount:     100,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e|f",
+				finishedPieceCount: 0,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(0.68)))
+			},
+		},
+		{
+			name: "evaluate without task piece count",
+			parent: &factor{
+				hostType:           cdnHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e|f",
+				totalUploadLoad:    100,
+				currentUploadLoad:  0,
+				finishedPieceCount: 20,
+				hostUUID:           "example",
+				taskPieceCount:     0,
+			},
+			child: &factor{
+				hostType:           clientHostType,
+				securityDomain:     "foo",
+				idc:                "bar",
+				location:           "a|b|c|d|e",
+				netTopology:        "a|b|c|d|e|f",
+				finishedPieceCount: 10,
+				hostUUID:           "example",
+			},
+			expect: func(t *testing.T, v float64) {
+				assert := assert.New(t)
+				assert.True(mathutils.EqualFloat64(v, float64(4.6)))
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			task := supervisor.NewTask(idgen.TaskID(mockTaskURL, nil), mockTaskURL, nil)
+
+			parentHost := supervisor.NewClientHost(
+				tc.parent.hostUUID, "", "", 0, 0,
+				tc.parent.securityDomain,
+				tc.parent.location,
+				tc.parent.idc,
+				supervisor.WithNetTopology(tc.parent.netTopology),
+				supervisor.WithTotalUploadLoad(tc.parent.totalUploadLoad),
+			)
+			parentHost.CurrentUploadLoad.Store(tc.parent.currentUploadLoad)
+			parent := supervisor.NewPeer(idgen.PeerID(mockIP), task, parentHost)
+			parent.TotalPieceCount.Store(tc.parent.finishedPieceCount)
+
+			childHost := supervisor.NewClientHost(
+				tc.parent.hostUUID, "", "", 0, 0,
+				tc.child.securityDomain,
+				tc.child.location,
+				tc.child.idc,
+				supervisor.WithNetTopology(tc.child.netTopology),
+			)
+			child := supervisor.NewPeer(idgen.PeerID(mockIP), task, childHost)
+			child.TotalPieceCount.Store(tc.child.finishedPieceCount)
+
+			e := NewEvaluatorBase()
+			tc.expect(t, e.Evaluate(parent, child, tc.parent.taskPieceCount))
+		})
+	}
+}
+
+func TestEvaluatorNeedAdjustParent(t *testing.T) {
+	tests := []struct {
+		name   string
+		parent *factor
+		child  *factor
+		expect func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer)
+	}{
+		{
+			name: "peer is CDN",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: cdnHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				assert.Equal(e.NeedAdjustParent(child), false)
+			},
+		},
+		{
+			name: "peer has no parent",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				assert.Equal(e.NeedAdjustParent(child), true)
+			},
+		},
+		{
+			name: "peer has done",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				assert.Equal(e.NeedAdjustParent(child), true)
+			},
+		},
+		{
+			name: "parent has leaved",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				child.SetParent(parent)
+				parent.Leave()
+				assert.Equal(e.NeedAdjustParent(child), true)
+			},
+		},
+		{
+			name: "empty costs",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				child.SetParent(parent)
+				assert.Equal(e.NeedAdjustParent(child), false)
+			},
+		},
+		{
+			name: "costs are not normal distribution and peer should not be scheduler",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				child.SetParent(parent)
+				child.SetPieceCosts([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}...)
+				assert.Equal(e.NeedAdjustParent(child), false)
+			},
+		},
+		{
+			name: "costs are not normal distribution and peer should be scheduler",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				child.SetParent(parent)
+				child.SetPieceCosts([]int{1, 2, 3, 4, 5, 6, 7, 8, 23}...)
+				assert.Equal(e.NeedAdjustParent(child), true)
+			},
+		},
+		{
+			name: "costs are normal distribution and peer should not be scheduler",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				child.SetParent(parent)
+				child.SetPieceCosts([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 5}...)
+				assert.Equal(e.NeedAdjustParent(child), false)
+			},
+		},
+		{
+			name: "costs are normal distribution and peer should be scheduler",
+			parent: &factor{
+				hostType: clientHostType,
+			},
+			child: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, parent *supervisor.Peer, child *supervisor.Peer) {
+				assert := assert.New(t)
+				child.SetParent(parent)
+				child.SetPieceCosts([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15}...)
+				assert.Equal(e.NeedAdjustParent(child), true)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			task := supervisor.NewTask(idgen.TaskID(mockTaskURL, nil), mockTaskURL, nil)
+
+			parentHost := supervisor.NewClientHost(uuid.NewString(), "", "", 0, 0, "", "", "")
+			parent := supervisor.NewPeer(idgen.PeerID(mockIP), task, parentHost)
+
+			var child *supervisor.Peer
+			if tc.child.hostType == cdnHostType {
+				childHost := supervisor.NewCDNHost(uuid.NewString(), "", "", 0, 0, "", "", "")
+				child = supervisor.NewPeer(idgen.CDNPeerID(mockIP), task, childHost)
+			} else {
+				childHost := supervisor.NewClientHost(uuid.NewString(), "", "", 0, 0, "", "", "")
+				child = supervisor.NewPeer(idgen.PeerID(mockIP), task, childHost)
+			}
+
+			e := NewEvaluatorBase()
+			tc.expect(t, e, parent, child)
+		})
+	}
+}
+
+func TestEvaluatorIsBadNode(t *testing.T) {
+	tests := []struct {
+		name   string
+		peer   *factor
+		expect func(t *testing.T, e Evaluator, peer *supervisor.Peer)
+	}{
+		{
+			name: "peer is bad",
+			peer: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, peer *supervisor.Peer) {
+				assert := assert.New(t)
+				peer.SetStatus(supervisor.PeerStatusFail)
+				assert.Equal(e.IsBadNode(peer), true)
+			},
+		},
+		{
+			name: "peer is CDN",
+			peer: &factor{
+				hostType: cdnHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, peer *supervisor.Peer) {
+				assert := assert.New(t)
+				assert.Equal(e.IsBadNode(peer), false)
+			},
+		},
+		{
+			name: "empty costs",
+			peer: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, peer *supervisor.Peer) {
+				assert := assert.New(t)
+				assert.Equal(e.IsBadNode(peer), false)
+			},
+		},
+		{
+			name: "costs length is available and peer is not bad node",
+			peer: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, peer *supervisor.Peer) {
+				assert := assert.New(t)
+				peer.SetPieceCosts([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}...)
+				assert.Equal(e.IsBadNode(peer), false)
+			},
+		},
+		{
+			name: "costs length is available and peer is bad node",
+			peer: &factor{
+				hostType: clientHostType,
+			},
+			expect: func(t *testing.T, e Evaluator, peer *supervisor.Peer) {
+				assert := assert.New(t)
+				peer.SetPieceCosts([]int{1, 2, 3, 4, 5, 6, 7, 8, 181}...)
+				assert.Equal(e.IsBadNode(peer), true)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			task := supervisor.NewTask(idgen.TaskID(mockTaskURL, nil), mockTaskURL, nil)
+
+			var peer *supervisor.Peer
+			if tc.peer.hostType == cdnHostType {
+				childHost := supervisor.NewCDNHost(uuid.NewString(), "", "", 0, 0, "", "", "")
+				peer = supervisor.NewPeer(idgen.CDNPeerID(mockIP), task, childHost)
+			} else {
+				childHost := supervisor.NewClientHost(uuid.NewString(), "", "", 0, 0, "", "", "")
+				peer = supervisor.NewPeer(idgen.PeerID(mockIP), task, childHost)
+			}
+
+			e := NewEvaluatorBase()
+			tc.expect(t, e, peer)
+		})
+	}
+}
diff --git a/scheduler/core/evaluator/evaluator_test.go b/scheduler/core/evaluator/evaluator_test.go
new file mode 100644
index 00000000..daaf7b44
--- /dev/null
+++ b/scheduler/core/evaluator/evaluator_test.go
@@ -0,0 +1,71 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package evaluator
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEvaluatorNew(t *testing.T) {
+	tests := []struct {
+		name      string
+		algorithm string
+		expect    func(t *testing.T, e interface{})
+	}{
+		{
+			name:      "new evaluator with default algorithm",
+			algorithm: "default",
+			expect: func(t *testing.T, e interface{}) {
+				assert := assert.New(t)
+				assert.Equal(reflect.TypeOf(e).Elem().Name(), "evaluatorBase")
+			},
+		},
+		{
+			name:      "new evaluator with machine learning algorithm",
+			algorithm: "ml",
+			expect: func(t *testing.T, e interface{}) {
+				assert := assert.New(t)
+				assert.Equal(reflect.TypeOf(e).Elem().Name(), "evaluatorBase")
+			},
+		},
+		{
+			name:      "new evaluator with plugin",
+			algorithm: "plugin",
+			expect: func(t *testing.T, e interface{}) {
+				assert := assert.New(t)
+				assert.Equal(reflect.TypeOf(e).Elem().Name(), "evaluatorBase")
+			},
+		},
+		{
+			name:      "new evaluator with empty string",
+			algorithm: "",
+			expect: func(t *testing.T, e interface{}) {
+				assert := assert.New(t)
+				assert.Equal(reflect.TypeOf(e).Elem().Name(), "evaluatorBase")
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			tc.expect(t, New(tc.algorithm))
+		})
+	}
+}
diff --git a/scheduler/core/evaluator/plugin_test.go b/scheduler/core/evaluator/plugin_test.go
new file mode 100644
index 00000000..aa6d9526
--- /dev/null
+++ b/scheduler/core/evaluator/plugin_test.go
@@ -0,0 +1,72 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package evaluator
+
+import (
+	"os"
+	"os/exec"
+	"path"
+	"testing"
+
+	testifyassert "github.com/stretchr/testify/assert"
+)
+
+func TestLoadPlugin(t *testing.T) {
+	assert := testifyassert.New(t)
+	defer func() {
+		os.Remove("./testdata/d7y-scheduler-plugin-evaluator.so")
+		os.Remove("./testdata/test")
+	}()
+
+	var (
+		cmd    *exec.Cmd
+		output []byte
+		wd     string
+		err    error
+	)
+
+	// build plugin
+	cmd = exec.Command("go", "build", "-buildmode=plugin", "-o=./testdata/d7y-scheduler-plugin-evaluator.so", "testdata/plugin/evaluator.go")
+	output, err = cmd.CombinedOutput()
+	assert.Nil(err)
+	if err != nil {
+		t.Fatalf(string(output))
+		return
+	}
+
+	// build test binary
+	cmd = exec.Command("go", "build", "-o=./testdata/test", "testdata/main.go")
+	output, err = cmd.CombinedOutput()
+	assert.Nil(err)
+	if err != nil {
+		t.Fatalf(string(output))
+		return
+	}
+
+	wd, err = os.Getwd()
+	assert.Nil(err)
+	wd = path.Join(wd, "testdata")
+
+	// execute test binary
+	cmd = exec.Command("./testdata/test", "-plugin-dir", wd)
+	output, err = cmd.CombinedOutput()
+	assert.Nil(err)
+	if err != nil {
+		t.Fatalf(string(output))
+		return
+	}
+}
diff --git a/scheduler/core/evaluator/testdata/main.go b/scheduler/core/evaluator/testdata/main.go
new file mode 100644
index 00000000..0ab8c10e
--- /dev/null
+++ b/scheduler/core/evaluator/testdata/main.go
@@ -0,0 +1,56 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"d7y.io/dragonfly/v2/internal/dfpath"
+	"d7y.io/dragonfly/v2/scheduler/core/evaluator"
+	"d7y.io/dragonfly/v2/scheduler/supervisor"
+)
+
+func init() {
+	flag.StringVar(&dfpath.PluginsDir, "plugin-dir", ".", "")
+}
+
+func main() {
+	flag.Parse()
+
+	e, err := evaluator.LoadPlugin()
+	if err != nil {
+		fmt.Printf("load plugin error: %s\n", err)
+		os.Exit(1)
+	}
+
+	if score := e.Evaluate(&supervisor.Peer{}, &supervisor.Peer{}, int32(0)); score != float64(1) {
+		fmt.Println("Evaluate failed")
+		os.Exit(1)
+	}
+
+	if ok := e.NeedAdjustParent(&supervisor.Peer{}); !ok {
+		fmt.Println("NeedAdjustParent failed")
+		os.Exit(1)
+	}
+
+	if ok := e.IsBadNode(&supervisor.Peer{}); !ok {
+		fmt.Println("IsBadNode failed")
+		os.Exit(1)
+	}
+}
diff --git a/scheduler/core/evaluator/testdata/plugin/evaluator.go b/scheduler/core/evaluator/testdata/plugin/evaluator.go
new file mode 100644
index 00000000..e8e35f82
--- /dev/null
+++ b/scheduler/core/evaluator/testdata/plugin/evaluator.go
@@ -0,0 +1,37 @@
+/*
+ *     Copyright 2020 The Dragonfly Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main
+
+import "d7y.io/dragonfly/v2/scheduler/supervisor"
+
+type evaluator struct{}
+
+func (e *evaluator) Evaluate(parent *supervisor.Peer, child *supervisor.Peer, taskPieceCount int32) float64 {
+	return float64(1)
+}
+
+func (e *evaluator) NeedAdjustParent(peer *supervisor.Peer) bool {
+	return true
+}
+
+func (e *evaluator) IsBadNode(peer *supervisor.Peer) bool {
+	return true
+}
+
+func DragonflyPluginInit(option map[string]string) (interface{}, map[string]string, error) {
+	return &evaluator{}, map[string]string{"type": "scheduler", "name": "evaluator"}, nil
+}
diff --git a/scheduler/supervisor/peer_test.go b/scheduler/supervisor/peer_test.go
index 89ad48be..1dda59c7 100644
--- a/scheduler/supervisor/peer_test.go
+++ b/scheduler/supervisor/peer_test.go
@@ -193,71 +193,6 @@ func TestPeer_Tree(t *testing.T) {
 	}
 }
 
-func TestPeer_Cost(t *testing.T) {
-	tests := []struct {
-		name          string
-		finishedCount []int32
-		cost          []int
-		expect        func(t *testing.T, peer *supervisor.Peer, cost []int)
-	}{
-		{
-			name:          "normal workflow",
-			finishedCount: []int32{2, 3, 4},
-			cost:          []int{3, 4, 5},
-			expect: func(t *testing.T, peer *supervisor.Peer, cost []int) {
-				assert := assert.New(t)
-
-				costFetch := peer.GetPieceCosts()
-				assert.ElementsMatch(costFetch, cost)
-
-				average, success := peer.GetPieceAverageCost()
-				assert.True(success)
-				assert.Equal(4, average)
-				assert.Equal(peer.SortedValue(), 4*HostMaxLoad+100)
-			},
-		},
-		{
-			name:          "no workflow will be neglected",
-			finishedCount: []int32{},
-			cost:          []int{},
-			expect: func(t *testing.T, peer *supervisor.Peer, cost []int) {
-				assert := assert.New(t)
-
-				average, success := peer.GetPieceAverageCost()
-				assert.False(success)
-				assert.Equal(0, average)
-			},
-		},
-		{
-			name: "long workflow will be clipped",
-			finishedCount: []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
-				11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22},
-			cost: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
-				11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22},
-			expect: func(t *testing.T, peer *supervisor.Peer, cost []int) {
-				assert := assert.New(t)
-
-				costFetch := peer.GetPieceCosts()
-				assert.ElementsMatch(costFetch, cost[2:])
-
-				average, success := peer.GetPieceAverageCost()
-				assert.True(success)
-				assert.Equal(12, average)
-			},
-		},
-	}
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			task := mockATask("task")
-			peer := mockAPeer("peer", task)
-			for i := 0; i < len(tc.finishedCount); i++ {
-				peer.UpdateProgress(tc.finishedCount[i], tc.cost[i])
-			}
-			tc.expect(t, peer, tc.cost)
-		})
-	}
-}
-
 func TestPeer_Status(t *testing.T) {
 	tests := []struct {
 		name       string