diff --git a/addons/performance-tuning/manifest.yml b/addons/performance-tuning/manifest.yml index 35415a8d..10d326a0 100644 --- a/addons/performance-tuning/manifest.yml +++ b/addons/performance-tuning/manifest.yml @@ -1,22 +1,23 @@ type: update -name: DB Cluster Tuning (Alpha) +name: ProxySQL Tuning id: proxysql-db-tune-addon logo: addons/performance-tuning/images/mysql-proxysql-tuning.png -description: Change ProxySQL and Database settings according to your particular use case -baseUrl: https://raw.githubusercontent.com/jelastic-jps/mysql-cluster/master/ -targetNodes: proxysql +description: Change ProxySQL settings according to your particular use case + +baseUrl: https://raw.githubusercontent.com/sych74/mysql-cluster/JE-66111/addons/performance-tuning -globals: - dbMaxConnections: 2048 +targetNodes: proxysql settings: settingsTune: - onBeforeInit: addons/performance-tuning/scripts/variablesManager.js + onBeforeInit: /scripts/variablesManager.js?_r=${fn.random} fields: - type: list caption: ProxySQL Variable name: varName default: mysql-monitor_connect_timeout + editable: true + forceSelection: true tooltip: text: | The behaviour of ProxySQL can be tweaked using global variables. There are 2 types of global variables, depending on which part of ProxySQL they control: @@ -39,6 +40,7 @@ settings: - type: spinner name: threadsNumber value: 4 + min: 1 - type: tooltip text: The number of background threads that ProxySQL uses in order to process MySQL traffic. hidden: false @@ -51,80 +53,108 @@ settings: min: 1 - type: tooltip text: ProxySQL maintains a pool of connections. A connection pool is a cache of database connections maintained so that the connections can be reused when future requests to the database are required. - hidden: false + hidden: false + - type: displayfield - - type: compositefield - defaultMargins: "0 12 0 0" - items: - - type: displayfield - markup: Primary Node - name: prmnode - - type: displayfield - markup: "" - cls: x-form-item-label - width: 70 - tooltip: Primary node weight for Select queries - - type: displayfield - markup: Secondary Node - - type: displayfield - markup: "" - cls: x-form-item-label - width: 70 - tooltip: Secondary node weight for Select queries - - type: compositefield - caption: Weights Ratio - defaultMargins: "0 12 0 0" - tooltip: - text: The bigger the weight of a server relative to other weights, the higher the probability of the server to be chosen from a hostgroup. ProxySQL default load-balancing algorithm is random-weighted. - minWidth: 370 - items: - - type: spinner - name: weightMaster - min: 1 - default: 50 - - type: displayfield - markup: "" - cls: x-form-item-label - width: 10 - - type: displayfield - markup: "/" - cls: x-form-item-label - width: 10 - - type: displayfield - markup: "" - cls: x-form-item-label - width: 10 - - type: spinner - name: weightSlave - min: 1 - default: 50 - - type: spinner - caption: DB Max Connections - name: dbMaxConnections - value: 2048 - min: 1 - tooltip: Maximum number of allowed connections to Database nodes buttons: - caption: Change Settings action: settingsUpdate settings: settingsTune +responses: + 98: + type: warning + message: | + An error occurs while updating the ProxySQL settiings. + Please check the **/var/log/jcm.log** log file for details. + 99: + type: warning + message: Add-on does not provide performance tuning for a non-cluster topologies. + +onInstall: + - getClusterScheme + - jcmLogPermission actions: - settingsUpdate: - - cmd[sqldb]: |- - sed -i s/^max_connections.*/max_connections=${settings.dbMaxConnections}/g /etc/mysql/conf.d/my_custom.cnf - user: root - - cmd[sqldb]: jem service restart - - cmd[proxysql]: mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "update global_variables set variable_value=${settings.varValue} where variable_name='${settings.varName}';LOAD MYSQL VARIABLES TO RUNTIME;SAVE MYSQL VARIABLES TO DISK;" - - cmd[proxysql]: mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "UPDATE global_variables SET variable_value=${settings.threadsNumber} WHERE variable_name = 'mysql-threads'; SAVE MYSQL VARIABLES TO DISK;"; - - cmd[proxysql]: mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "UPDATE mysql_servers SET weight=${settings.weightMaster} WHERE hostname='node${nodes.sqldb.master.id}' and hostgroup_id='11';LOAD MYSQL SERVERS TO RUNTIME;SAVE MYSQL SERVERS TO DISK;" - - cmd[proxysql]: mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "UPDATE mysql_servers SET weight=1 WHERE hostname='node${nodes.sqldb.master.id}' and hostgroup_id='10';LOAD MYSQL SERVERS TO RUNTIME;SAVE MYSQL SERVERS TO DISK;" - - forEach(sql:nodes.sqldb): - if ('${@sql.displayName}' == "Secondary"): - cmd[proxysql]: mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "UPDATE mysql_servers SET weight=${settings.weightSlave} WHERE hostname='node${@sql.id}';LOAD MYSQL SERVERS TO RUNTIME;SAVE MYSQL SERVERS TO DISK;" - - cmd[proxysql]: mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "UPDATE mysql_servers SET max_connections=${settings.maxConnections};LOAD MYSQL SERVERS TO RUNTIME;SAVE MYSQL SERVERS TO DISK;" - - cmd[proxysql]: |- - jem service restart - user: root + + jcmLogPermission: + cmd[proxy]: |- + echo "JCM add-on logs" > /var/log/jcm.log; + chown jelastic /var/log/jcm.log; + user: root + + getClusterScheme: + - script: | + var nodeGroups, resp, scheme = ""; + resp = api.env.control.GetNodeGroups("${env.name}", session); + if (resp.result != 0) return resp; + nodeGroups = resp.object; + for (var i = 0, n = nodeGroups.length; i < n; i++) { + if (nodeGroups[i].name == 'sqldb' && nodeGroups[i].cluster && nodeGroups[i].cluster.enabled && nodeGroups[i].cluster.settings.scheme) { + scheme = String(nodeGroups[i].cluster.settings.scheme); + } + } + if (scheme !== "master" && scheme !== "slave" && scheme !== "galera") return 99; + return {result: 0, onAfterReturn:{ setGlobals:{ "scheme": scheme }}} + + settingsUpdate: + - variableUpdate + - weightUpdate + - getClusterScheme + - if ('${globals.scheme}' == 'galera'): + setGaleraMaxWriters + - return: + type: success + message: ProxySQL settings were updated successfully. + + variableUpdate: + cmd[proxysql]: |- + curl -fsSL 'https://raw.githubusercontent.com/sych74/mysql-cluster/JE-66111/addons/performance-tuning/scripts/jcm.sh' -o /tmp/jcm.sh; + bash /tmp/jcm.sh setMySQLThreads --value=${settings.threadsNumber}; + mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "update global_variables set variable_value=${settings.varValue} where variable_name='${settings.varName}';LOAD MYSQL VARIABLES TO RUNTIME;SAVE MYSQL VARIABLES TO DISK;"; + mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e "UPDATE mysql_servers SET max_connections=${settings.maxConnections}; LOAD MYSQL SERVERS TO RUNTIME;SAVE MYSQL SERVERS TO DISK;"; + + weightUpdate: + script: | + let envInfo, weight, cmd, execCmd, nodeId, settings = JSON.parse('${settings}'); + let minWeight = 0; + let maxWeight = 10000000; + + function percentToWeight(percent){ + let localWeight=1; + let parsedPercent = parseInt(percent); + if (parsedPercent == 0) localWeight = 0; + if (parsedPercent == 1) localWeight = 100000; + if (parsedPercent > 1) localWeight = (parsedPercent / 100) * maxWeight; + return { + result: 0, + weight: localWeight + } + }; + + envInfo = api.env.control.GetEnvInfo("${env.name}", session); + if (envInfo.result != 0) return envInfo; + for (let i = 0, n = envInfo.nodes.length; i < n; i++) { + if (envInfo.nodes[i].nodeGroup == 'sqldb') { + nodeId = envInfo.nodes[i].id; + weight = percentToWeight(settings[nodeId]).weight; + cmd = "bash /tmp/jcm.sh setWeight --weight=" + weight+ " --node=node" +nodeId; + execCmd = api.environment.control.ExecCmdByGroup({ + envName: '${env.envName}', + nodeGroup: 'proxy', + commandList: toJSON([{"command": cmd}]) + }); + if (execCmd.result != 0) return execCmd; + } + } + return {result : 0}; + + setGaleraMaxWriters: + script: | + var cmd = "bash /tmp/jcm.sh setGaleraMaxWriters --count=" + '${settings.maxWriters}'; + return api.environment.control.ExecCmdByGroup({ + envName: '${env.envName}', + nodeGroup: 'proxy', + commandList: toJSON([{"command": cmd1}]) + }); diff --git a/addons/performance-tuning/scripts/jcm.sh b/addons/performance-tuning/scripts/jcm.sh new file mode 100644 index 00000000..ab83760f --- /dev/null +++ b/addons/performance-tuning/scripts/jcm.sh @@ -0,0 +1,486 @@ +#!/bin/bash + +USER_SCRIPT_PATH="{URL}" + +PLATFORM_DOMAIN="{PLATFORM_DOMAIN}" + +PROMOTE_NEW_PRIMARY_FLAG="/var/lib/jelastic/promotePrimary" + +JCM_CONFIG="/etc/proxysql/jcm.conf" +ITERATION_CONFIG="/tmp/iteration.conf" + +SUCCESS_CODE=0 +FAIL_CODE=99 +RUN_LOG=/var/log/jcm.log + +WRITE_HG_ID=10 +READ_HG_ID=11 +MAX_REPL_LAG=20 + +log(){ + local message=$1 + local timestamp + timestamp=`date "+%Y-%m-%d %H:%M:%S"` + echo -e "[${timestamp}]: ${message}" >> ${RUN_LOG} +} + +execResponse(){ + local result=$1 + local message=$2 + local output_json="{\"result\": ${result}, \"out\": \"${message}\"}" + echo $output_json +} + +proxyCommandExec(){ + local command="$1" + MYSQL_PWD=admin mysql -uadmin -h127.0.0.1 -P6032 -BNe "$command" +} + +execAction(){ + local action="$1" + local message="$2" + stdout=$( { ${action}; } 2>&1 ) && { log "${message}...done"; } || { + log "${message}...failed\n${stdout}\n"; + error="${message} failed, please check ${RUN_LOG} for details" + execResponse "$FAIL_CODE" "$error"; + exit 0; + } +} + +execReturn(){ + local action="$1" + local message="$2" + stdout=$( { ${action}; } 2>&1 ) && { log "${message}...done"; } || { + log "${message}...failed\n${stdout}\n"; + error="${message} failed, please check ${RUN_LOG} for details" + execResponse "$FAIL_CODE" "$error"; + exit 0; + } +} + +primaryStatus(){ + local cmd="select status from runtime_mysql_servers where hostgroup_id=$WRITE_HG_ID;" + local status=$(proxyCommandExec "$cmd") + source $JCM_CONFIG; + source $ITERATION_CONFIG; + if [[ "x$status" != "xONLINE" ]] && [[ ! -f $PROMOTE_NEW_PRIMARY_FLAG ]]; then + if [[ $ITERATION -eq $ONLINE_ITERATIONS ]]; then + log "Primary node status is OFFLINE" + log "Promoting new Primary" + echo "ITERATION=0" > ${ITERATION_CONFIG}; + curl --location --request POST "${PLATFORM_DOMAIN}1.0/environment/node/rest/sendevent" --data-urlencode "params={'name': 'executeScript'}" + else + ITERATION=$(($ITERATION+1)) + echo "ITERATION=$ITERATION" > ${ITERATION_CONFIG}; + fi + else + if [ ! -f $PROMOTE_NEW_PRIMARY_FLAG ]; then + log "Primary node status is ONLINE" + echo "ITERATION=0" > ${ITERATION_CONFIG}; + else + log "Promoting new Primary in progress" + fi + fi +} + +addNodeToWriteGroup(){ + local nodeId="$1" + local cmd="INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES ($WRITE_HG_ID, '$nodeId', 3306);" + proxyCommandExec "$cmd" +} + +addNodeToReadGroup(){ + local nodeId="$1" + local cmd="INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_replication_lag) VALUES ($READ_HG_ID, '$nodeId', 3306, '$MAX_REPL_LAG');" + proxyCommandExec "$cmd" +} + +loadServersToRuntime(){ + local cmd="LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;" + proxyCommandExec "$cmd" +} + +loadVariablesToRuntime(){ + local cmd="LOAD MYSQL VARIABLES TO RUNTIME;" + proxyCommandExec "$cmd" +} + +saveVariablesToDisk(){ + local cmd="SAVE MYSQL VARIABLES TO DISK;" + proxyCommandExec "$cmd" +} + +addSchedulerProxy(){ + local interval_ms="$1" + local filename="$2" + local arg1="$3" + local comment="$4" + local cmd="INSERT INTO scheduler(interval_ms,filename,arg1,active,comment) " + cmd+="VALUES ($interval_ms,'$filename', '$arg1',1,'$comment');" + proxyCommandExec "$cmd" +} + +deleteAllSchedulers(){ + local cmd="DELETE from scheduler;" + proxyCommandExec "$cmd" +} + +updateSchedulerProxy(){ + local interval_ms="$1" + local comment="$2" + local cmd="UPDATE scheduler SET interval_ms=$interval_ms WHERE comment='$comment';" + proxyCommandExec "$cmd" +} + +loadSchedulerToRuntime(){ + local cmd="LOAD SCHEDULER TO RUNTIME; SAVE SCHEDULER TO DISK;" + proxyCommandExec "$cmd" +} + +setSchedulerTimeout(){ + for i in "$@"; do + case $i in + --interval=*) + INTERVAL=${i#*=} + shift + shift + ;; + + --scheduler_name=*) + SCHEDULER_NAME=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + + local interval_ms=5000 + local interval_sec=5 + local online_iterations=$((${INTERVAL}/${interval_sec})) + + execAction "updateParameterInConfig ONLINE_ITERATIONS $online_iterations" "Set $online_iterations iterations checks in the $JCM_CONFIG" + execAction "loadSchedulerToRuntime" "Loading cronjob tasks to runtime" +} + +addScheduler(){ + for i in "$@"; do + case $i in + --interval=*) + INTERVAL=${i#*=} + shift + shift + ;; + + --filename=*) + FILENAME=${i#*=} + shift + shift + ;; + + --arg1=*) + ARG1=${i#*=} + shift + shift + ;; + + --arg2=*) + ARG2=${i#*=} + shift + shift + ;; + + --arg3=*) + ARG3=${i#*=} + shift + shift + ;; + + --arg4=*) + ARG4=${i#*=} + shift + shift + ;; + + --arg5=*) + ARG5=${i#*=} + shift + shift + ;; + + --scheduler_name=*) + SCHEDULER_NAME=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + local interval_ms=5000 + local interval_sec=5 + local online_iterations=$((${INTERVAL}/${interval_sec})) + + execAction "deleteAllSchedulers" "Delete Schedulers" + execAction "loadSchedulerToRuntime" "Loading cronjob tasks to runtime" + execAction "updateParameterInConfig ONLINE_ITERATIONS $online_iterations" "Set $online_iterations iterations checks in the $JCM_CONFIG" + execAction "addSchedulerProxy $interval_ms $FILENAME $ARG1 $SCHEDULER_NAME" "Adding $SCHEDULER_NAME crontask to scheduler" + execAction "loadSchedulerToRuntime" "Loading cronjob tasks to runtime" + +} + +deletePrimary(){ + local nodeId="$1" + local cmd="DELETE from mysql_servers WHERE hostname='$nodeId';" + proxyCommandExec "$cmd" +} + +updateParameterInConfig(){ + local parameter="$1" + local value="$2" + grep -q "$parameter" ${JCM_CONFIG} && { sed -i "s/${parameter}.*/$parameter=$value/" ${JCM_CONFIG}; } || { echo "$parameter=$value" >> ${JCM_CONFIG}; } +} + +newPrimary(){ + for i in "$@"; do + case $i in + --server=*) + SERVER=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + if [[ -f $JCM_CONFIG ]]; then + source $JCM_CONFIG; + execAction "deletePrimary $PRIMARY_NODE_ID" "Deleting primary node $PRIMARY_NODE_ID from configuration" + execAction "loadServersToRuntime" "Loading server configuration to runtime" + fi + execAction "addNodeToWriteGroup $SERVER" "Adding $SERVER to writer hostgroup" +# execAction "addNodeToReadGroup $SERVER" "Adding $SERVER to reader hostgroup" + execAction "loadServersToRuntime" "Loading server configuration to runtime" + execAction "updateParameterInConfig PRIMARY_NODE_ID $SERVER" "Set primary node to $SERVER in the $JCM_CONFIG" +} + +getGlobalVariables(){ + local cmd="SELECT variable_name,variable_value FROM global_variables WHERE variable_name LIKE 'mysql-%' and variable_value <> '' AND variable_value IS NOT NULL;" + local global_variables=$(proxyCommandExec "$cmd") + local json_variables=$(jq -n '[]') + + while IFS=$'\t' read -r variable variable_value; + do + json_variables=$(echo $json_variables | jq \ + --arg variable_name "$variable" \ + --arg variable_value "$variable_value" \ + '. += [{"variable_name": $variable_name, "variable_value": $variable_value}]') + done <<< "$global_variables" + echo $json_variables +} + +setGlobalVariable(){ + for i in "$@"; do + case $i in + --variable-name=*) + VARIABLE_NAME=${i#*=} + shift + shift + ;; + --variable-value=*) + VARIABLE_VALUE=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + _set_global_variable(){ + local variable="$1" + local value="$2" + local cmd="UPDATE global_variables SET variable_value='$value' WHERE variable_name='$variable';" + proxyCommandExec "$cmd" + } + execAction "_set_global_variable $VARIABLE_NAME $VARIABLE_VALUE" "Set global variable ${VARIABLE_NAME}=${VARIABLE_VALUE}" + execAction "loadVariablesToRuntime" "Loading global variables to runtime" + execAction "saveVariablesToDisk" "Save global variables to disk" +} + +getMySQLThreads(){ + local cmd="SELECT variable_value FROM global_variables WHERE variable_name='mysql-threads';" + local mysql_threads=$(proxyCommandExec "$cmd") + echo $mysql_threads +} + + +setMySQLThreads(){ + for i in "$@"; do + case $i in + --value=*) + VALUE=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + _set_mysql_threads(){ + local value="$1" + local cmd="UPDATE global_variables SET variable_value=$value WHERE variable_name='mysql-threads';" + proxyCommandExec "$cmd" + } + local old_value=$(getMySQLThreads) + if [ "x$old_value" != "x$VALUE" ]; + then + execAction "_set_mysql_threads $VALUE" "Set mysql threads value to $VALUE" + execAction "saveVariablesToDisk" "Save global variables to disk" + sudo jem service restart; + fi; +} + +getWeight(){ + for i in "$@"; do + case $i in + --node=*) + NODE=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + local cmd="SELECT weight from mysql_servers where hostname = '$NODE';" + local weight=$(proxyCommandExec "$cmd") + echo $weight +} + +setWeight(){ + for i in "$@"; do + case $i in + --node=*) + NODE=${i#*=} + shift + shift + ;; + --weight=*) + WEIGHT=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + _set_weight(){ + local node="$1" + local weight="$2" + local cmd="UPDATE mysql_servers SET weight=$weight WHERE hostname='$node';" + proxyCommandExec "$cmd" + } + execAction "_set_weight $NODE $WEIGHT" "Set weight $WEIGHT for $NODE node" + execAction "loadServersToRuntime" "Loading mysql servers to runtime" +} + +getGaleraMaxWriters(){ + local cmd="SELECT max_writers FROM mysql_galera_hostgroups;" + local output=$(proxyCommandExec "$cmd") + echo $output +} + +setGaleraMaxWriters(){ + for i in "$@"; do + case $i in + --count=*) + COUNT=${i#*=} + shift + shift + ;; + *) + ;; + esac + done + + _set_max_writers(){ + local count="$1" + local cmd="UPDATE mysql_galera_hostgroups SET max_writers=$count;" + proxyCommandExec "$cmd" + } + execAction "_set_max_writers $COUNT" "Set max writers count for galera cluster" + execAction "loadServersToRuntime" "Loading mysql servers to runtime" +} + +forceFailover(){ + curl --location --request POST "${PLATFORM_DOMAIN}1.0/environment/node/rest/sendevent" --data-urlencode "params={'name': 'executeScript'}" +} + +case ${1} in + + forceFailover) + forceFailover + ;; + + primaryStatus) + primaryStatus + ;; + + newPrimary) + newPrimary "$@" + ;; + + addScheduler) + addScheduler "$@" + ;; + + setSchedulerTimeout) + setSchedulerTimeout "$@" + ;; + + updateParameterInConfig) + updateParameterInConfig "$@" + ;; + + getGlobalVariables) + getGlobalVariables + ;; + + setGlobalVariable) + setGlobalVariable "$@" + ;; + + getWeight) + getWeight "$@" + ;; + + setWeight) + setWeight "$@" + ;; + + getGaleraMaxWriters) + getGaleraMaxWriters + ;; + + setGaleraMaxWriters) + setGaleraMaxWriters "$@" + ;; + + getMySQLThreads) + getMySQLThreads + ;; + + setMySQLThreads) + setMySQLThreads "$@" + ;; + + *) + echo "Please use $(basename "$BASH_SOURCE") primaryStatus or $(basename "$BASH_SOURCE") newPrimary" +esac diff --git a/addons/performance-tuning/scripts/variablesManager.js b/addons/performance-tuning/scripts/variablesManager.js index 76cd2548..a40532f4 100644 --- a/addons/performance-tuning/scripts/variablesManager.js +++ b/addons/performance-tuning/scripts/variablesManager.js @@ -1,248 +1,285 @@ function ApplySQLVariable() { - let envName = "${env.envName}"; - let varName = "varName"; - let varValue = "varValue"; - - let threadsNumber = "threadsNumber"; - let MYSQL_THREADS = "mysql-threads"; - - let MAX_CONNECTIONS = "mysql-max_connections"; - let maxConnections = "maxConnections"; - let dbMaxConnections = "dbMaxConnections"; - - let ROOT = "root"; - let MY_CNF = "/etc/my.cnf"; - let envInfo; - let SQLDB = "sqldb"; - let PRIMARY = "Primary"; - let SECONDARY = "Secondary"; - - this.run = function() { - let resp = this.defineMyCNF(); - if (resp.result != 0) return resp; - - resp = this.defineWeights(); - if (resp.result != 0) return resp; - - return this.getVariables(); - }; - - this.getVariables = function() { - let command = "curl -fsSL 'https://github.com/jelastic-jps/mysql-cluster/raw/master/addons/promote-new-primary/scripts/jcm.sh' -o jcm.sh\n" + - "bash jcm.sh getGlobalVariables" - let resp = this.cmdById("${nodes.proxy.master.id}", command); - if (resp.result != 0) return resp; - - let variables = JSON.parse(resp.responses[0].out); - - resp = this.formatVariables(variables); - if (resp.result != 0) return resp; - - settings = settings || {}; - settings.fields = settings.fields || {}; - - let field; - for (let i = 0, n = settings.fields.length; i < n; i++) { - field = settings.fields[i]; - if (field.name == varName) { - field.values = resp.variables; - } - - if (field.name == varValue) { - field.dependsOn = resp.dependsData; - } - - if (field.caption == "ProxySQL Threads" && this.getMySQLThreads()) { - if (field.items && field.items[0]) { - field.items[0].value = this.getMySQLThreads(); - } - } - - if (field.name == maxConnections && this.getMaxConnections()) { - field.value = this.getMaxConnections(); - } - - if (field.name == dbMaxConnections && this.getDbMaxConnections()) { - field.value = this.getDbMaxConnections(); - } - - if (field.caption == "Weights Ratio" && this.getWeights()) { - if (field.items) { - let item; - for (let k = 0, l = field.items.length; k < l; k++) { - item = field.items[k]; - if (item) { - if (item.name == "weightMaster") { - item.value = parseInt(this.getWeights().primary); - } else if (item.name == "weightSlave") { - item.value = parseInt(this.getWeights().secondary); - } - } - } - } - } - } - - return settings; - }; - - this.defineWeights = function() { - let resp = this.getNodesByGroup(SQLDB); - if (resp.result != 0) return resp; - - let primaryWeight = ""; - let secondaryWeight = ""; - - let nodes = resp.nodes; - - for (let i = 0, n = nodes.length; i < n; i++) { - if (nodes[i].displayName == SECONDARY) { - resp = this.getWeight(nodes[i].id); - if (resp.result != 0) return resp; - - secondaryWeight = resp.weight; - } - - if (nodes[i].displayName == PRIMARY) { - resp = this.getWeight(nodes[i].id); - if (resp.result != 0) return resp; - - primaryWeight = resp.weight; - } - } - - this.setWeights({ - primary: primaryWeight, - secondary: secondaryWeight - }); - - return { result: 0 } - }; - - this.getWeight = function(id) { - let command = "mysql -uadmin -padmin -h 127.0.0.1 -P6032 -e \"select weight from mysql_servers where hostname = 'node" + id + "';\" | sed '2,4!d' | tail -n 1"; - let resp = this.cmdById("${nodes.proxy.master.id}", command); - if (resp.result != 0) return resp; - return { - result: 0, - weight: resp.responses[0].out - } - }; - - this.getEnvInfo = function() { - if (!envInfo) { - envInfo = api.env.control.GetEnvInfo("${env.name}", session); - if (envInfo.result != 0) return envInfo; - - } - - return envInfo; - }; - - this.getNodesByGroup = function(group) { - envInfo = this.getEnvInfo(); - if (envInfo.result != 0) return envInfo; - - let nodes = []; - - for (let i = 0, n = envInfo.nodes.length; i < n; i++) { - if (envInfo.nodes[i].nodeGroup == group) { - nodes.push(envInfo.nodes[i]); - } - } - - return { - result: 0, - nodes: nodes - } - }; - - this.defineMyCNF = function() { - let command = "grep -q 'max_connections' " + MY_CNF + " && { grep -r 'max_connections' " + MY_CNF + " | cut -c 17- || echo \"\"; } || { sed -i \"s|\\[mysqld\\]|\\[mysqld\\]\\nmax_connections=2048|g\" " + MY_CNF + "; echo 2048; };"; - let resp = this.cmdById("${nodes.proxy.master.id}", command); - if (resp.result != 0) return resp; - - let max_connections = resp.responses[0].out; - this.setDbMaxConnections(parseInt(max_connections)); - - return { - result: 0 - }; - }; - - this.formatVariables = function(variables) { - let dependsData = {}; - let dependsValues = {}; - let formatedData = []; - let variable; - - for (let i = 0 , n = variables.length; i < n; i++) { - variable = variables[i]; - formatedData.push({ - caption: variable.variable_name, - value: variable.variable_name - }); - - dependsData[variable.variable_name] = [{ - caption: variable.variable_value, - value: variable.variable_value - }]; - - if (variable.variable_name == MYSQL_THREADS) { - this.setMySQLThreads(variable.variable_value); - } - - if (variable.variable_name == MAX_CONNECTIONS) { - this.setMaxConnections(variable.variable_value); - } - } - - if (dependsData) { - dependsValues[varName] = dependsData; + let envName = "${env.envName}"; + let varName = "varName"; + let varValue = "varValue"; + + let minWeight = 0; + let maxWeight = 10000000; + + let threadsNumber = "threadsNumber"; + let MYSQL_THREADS = "mysql-threads"; + + let MAX_CONNECTIONS = "mysql-max_connections"; + let maxConnections = "maxConnections"; + let dbMaxConnections = "dbMaxConnections"; + + let ROOT = "root"; + let MY_CNF = "/etc/my.cnf"; + let envInfo; + let SQLDB = "sqldb"; + let PRIMARY = "Primary"; + let SECONDARY = "Secondary"; + + this.run = function() { + let resp = this.defineMyCNF(); + if (resp.result != 0) return resp; + + return this.getVariables(); + }; + + this.getScheme = function() { + let nodeGroups, resp, scheme = ""; + + resp = api.env.control.GetNodeGroups("${env.name}", session); + if (resp.result != 0) return resp; + + nodeGroups = resp.object; + for (var i = 0, n = nodeGroups.length; i < n; i++) { + if (nodeGroups[i].name == 'sqldb' && nodeGroups[i].cluster && nodeGroups[i].cluster.enabled && nodeGroups[i].cluster.settings.scheme) { + scheme = String(nodeGroups[i].cluster.settings.scheme); + } + } + return scheme; + }; + + this.weightToPercent = function(weight) { + let percent; + parsedWeight = parseInt(weight); + if (parsedWeight == minWeight) percent = 0; + if (parsedWeight >= 1 && parsedWeight <= 100000) percent = 1; + if (parsedWeight > 100000) percent = (parsedWeight / maxWeight) * 100; + return { + result: 0, + percent: percent + } + }; + + this.getVariables = function() { + let command = "curl -fsSL 'https://raw.githubusercontent.com/sych74/mysql-cluster/JE-66111/addons/performance-tuning/scripts/jcm.sh' -o /tmp/jcm.sh\n" + + "bash /tmp/jcm.sh getGlobalVariables" + let resp = this.cmdById("${nodes.proxy.master.id}", command); + if (resp.result != 0) return resp; + + let variables = JSON.parse(resp.responses[0].out); + + resp = this.formatVariables(variables); + if (resp.result != 0) return resp; + + settings = settings || {}; + fields = settings.fields || {}; + + let field; + for (let i = 0, n = fields.length; i < n; i++) { + field = fields[i]; + + if (field.name == varName) { + field.values = resp.variables; + } + + if (field.name == varValue) { + field.dependsOn = resp.dependsData; + } + + if (field.caption == "ProxySQL Threads" && this.getMySQLThreads()) { + if (field.items && field.items[0]) { + field.items[0].value = this.getMySQLThreads(); } - - return { - result: 0, - variables: formatedData, - dependsData: dependsValues - } - }; - - this.getDbMaxConnections = function() { - return this.dbMaxConnections; - }; - - this.setDbMaxConnections = function(connections) { - this.dbMaxConnections = connections; - }; - - this.getWeights = function() { - return this.weights; - }; - - this.setWeights = function(weights) { - this.weights = weights; - }; - - this.getMySQLThreads = function() { - return this.mysqlThreads; - }; - - this.setMySQLThreads = function(threads) { - this.mysqlThreads = threads; - }; - - this.getMaxConnections = function() { - return this.maxConnections; - }; - - this.setMaxConnections = function(connections) { - this.maxConnections = connections; - }; - - this.cmdById = function(id, command) { - return api.env.control.ExecCmdById(envName, session, id, toJSON([{ command: command }]), true, ROOT); + } + + if (field.name == maxConnections && this.getMaxConnections()) { + field.value = this.getMaxConnections(); + } + } + + resp = this.getNodesByGroup(SQLDB); + if (resp.result != 0) return resp; + + let nodes = resp.nodes.sort(function (a, b) { return a.id - b.id; }); + let scheme = this.getScheme(); + + if (scheme == "galera"){ + fields.push({ + "type": "compositefield", + "caption": "Write nodes count", + "items": [{ + "type": "spinner", + "name": "maxWriters", + "value": this.getGaleraMaxWriters().maxWriters, + "min": 1, + "max": nodes.length + } , { + "type": "tooltip", + "text": "This value determines the maximum number of nodes that should be allowed in the writer_hostgroup.", + "hidden": false + }] + }, { + "type": "displayfield" + }); + } + + fields.push({ + "type": "compositefield", + "defaultMargins": "0 12 0 0", + "items": [{ + "type": "displayfield", + "markup": "Weights Ratio 1-100", + "name": "prmnode" + }, { + "type": "tooltip", + "text": "The bigger the weight of a server relative to other weights, the higher the probability of the server to be chosen from a hostgroup. ProxySQL default load-balancing algorithm is random-weighted.", + "hidden": false + }] + }); + + for (let i = 0, n = nodes.length; i < n; i++) { + fields.push({ + "type": "compositefield", + "caption": nodes[i].displayName+" node"+nodes[i].id, + "items": [{ + "type": "spinner", + "name": nodes[i].id, + "value": this.weightToPercent(this.getWeight(nodes[i].id).weight).percent, + "min": 0, + "max": 100 + }] + }); + } + return settings; + }; + + this.getWeight = function(id) { + let command = "bash /tmp/jcm.sh getWeight --node=node"+id; + let resp = this.cmdById("${nodes.proxy.master.id}", command); + if (resp.result != 0) return resp; + return { + result: 0, + weight: resp.responses[0].out + } + }; + + this.getGaleraMaxWriters = function() { + let command = "bash /tmp/jcm.sh getGaleraMaxWriters"; + let resp = this.cmdById("${nodes.proxy.master.id}", command); + if (resp.result != 0) return resp; + return { + result: 0, + maxWriters: resp.responses[0].out + } + }; + + this.getEnvInfo = function() { + if (!envInfo) { + envInfo = api.env.control.GetEnvInfo("${env.name}", session); + if (envInfo.result != 0) return envInfo; + } + + return envInfo; + }; + + this.getNodesByGroup = function(group) { + envInfo = this.getEnvInfo(); + if (envInfo.result != 0) return envInfo; + + let nodes = []; + + for (let i = 0, n = envInfo.nodes.length; i < n; i++) { + if (envInfo.nodes[i].nodeGroup == group) { + nodes.push(envInfo.nodes[i]); + } + } + + return { + result: 0, + nodes: nodes + } + }; + + this.defineMyCNF = function() { + let command = "grep -q 'max_connections' " + MY_CNF + " && { grep -r 'max_connections' " + MY_CNF + " | cut -c 17- || echo \"\"; } || { sed -i \"s|\\[mysqld\\]|\\[mysqld\\]\\nmax_connections=2048|g\" " + MY_CNF + "; echo 2048; };"; + let resp = this.cmdById("${nodes.proxy.master.id}", command); + if (resp.result != 0) return resp; + + let max_connections = resp.responses[0].out; + this.setDbMaxConnections(parseInt(max_connections)); + + return { + result: 0 }; + }; + + this.formatVariables = function(variables) { + let dependsData = {}; + let dependsValues = {}; + let formatedData = []; + let variable; + + for (let i = 0 , n = variables.length; i < n; i++) { + variable = variables[i]; + formatedData.push({ + caption: variable.variable_name, + value: variable.variable_name + }); + + dependsData[variable.variable_name] = [{ + caption: variable.variable_value, + value: variable.variable_value + }]; + + if (variable.variable_name == MYSQL_THREADS) { + this.setMySQLThreads(variable.variable_value); + } + + if (variable.variable_name == MAX_CONNECTIONS) { + this.setMaxConnections(variable.variable_value); + } + } + + if (dependsData) { + dependsValues[varName] = dependsData; + } + + return { + result: 0, + variables: formatedData, + dependsData: dependsValues + } + }; + + this.getDbMaxConnections = function() { + return this.dbMaxConnections; + }; + + this.setDbMaxConnections = function(connections) { + this.dbMaxConnections = connections; + }; + + this.getWeights = function() { + return this.weights; + }; + + this.setWeights = function(weights) { + this.weights = weights; + }; + + this.getMySQLThreads = function() { + return this.mysqlThreads; + }; + + this.setMySQLThreads = function(threads) { + this.mysqlThreads = threads; + }; + + this.getMaxConnections = function() { + return this.maxConnections; + }; + + this.setMaxConnections = function(connections) { + this.maxConnections = connections; + }; + + this.cmdById = function(id, command) { + return api.env.control.ExecCmdById(envName, session, id, toJSON([{ command: command }]), true, ROOT); + }; }; function log(message) { diff --git a/addons/promote-new-primary/scripts/promote-master.js b/addons/promote-new-primary/scripts/promote-master.js index c25491a2..4ba74da7 100644 --- a/addons/promote-new-primary/scripts/promote-master.js +++ b/addons/promote-new-primary/scripts/promote-master.js @@ -80,6 +80,10 @@ function promoteNewPrimary() { resp = this.addIteration(true); if (resp.result != 0) return resp; return this.setIsRunningStatus(false); + } else { + let nodeGroup = this.getAddOnType() ? PROXY : SQLDB; + + return this.cmdByGroup("rm -rf " + TMP_FILE, nodeGroup, 3); } return { result: 0 } @@ -636,7 +640,7 @@ function promoteNewPrimary() { } } - let resp = api.env.control.ChangeTopology({ + return api.env.control.ChangeTopology({ envName: envName, session: session, env: { @@ -645,9 +649,6 @@ function promoteNewPrimary() { }, nodes: nodes }); - if (resp.result != 0) return resp; - - return this.cmdByGroup("rm -rf " + TMP_FILE, SQLDB, 3); }; this.removeFailedPrimary = function() { @@ -670,7 +671,7 @@ function promoteNewPrimary() { if (nodeGroup == PROXY && !test) { let resp = this.checkNodesAvailability(PROXY); - if (resp.nodeid) { + if (resp && resp.nodeid) { return this.cmdById(resp.nodeid, command); } } diff --git a/addons/recovery/README.md b/addons/recovery/README.md index 780f58bb..3be98700 100644 --- a/addons/recovery/README.md +++ b/addons/recovery/README.md @@ -45,11 +45,13 @@ Click **Install** and wait for the successful installation. The add-on will appe The add-on allows doing two actions: -- **Cluster Diagnostic** - with this action, the add-on automatically scans all nodes in the cluster in order to identify whether the nodes are accessible and whether databases are consistent. If during diagnostic the database corruption or even node failure is detected, the add-on will warn you with a respective popup window: +- **Cluster Diagnostic** - with this action, the add-on automatically scans all nodes in the cluster in order to identify whether the nodes are accessible and whether databases are consistent. If during diagnostic the database corruption or even node failure is detected, the add-on will warn you with a respective pop-up window: ![diagnostic failure](images/04-diagnostic-failure.png) -- **Cluster Recovery** - once any failure has been detected, you can either try automatic database recovery by pressing the **Cluster Recovery** button or perform manual database recovery by following the link to the recovery guide. The best practice is to use the automatic recovery scenario. +> **Tip:** Cluster diagnostic is triggered automatically before the **environment stop** and **cloudlets change** operations. The respective actions will proceed only if the database integrity is verified (to avoid additional damage to the cluster). + +- **Cluster Recovery** - if any failure has been detected, you can either try automatic database recovery by pressing the **Cluster Recovery** button or perform manual database recovery by following the link to the recovery guide. The best practice is to use the automatic recovery scenario. To perform automatic recovery, provide database user credentials either you got upon database cluster installation or the credentials of another privileged user you created. diff --git a/manifest.jps b/manifest.jps index 2a5ce03d..d0888f5c 100644 --- a/manifest.jps +++ b/manifest.jps @@ -10,7 +10,7 @@ categories: description: text: "/texts/description.md?_r=1" - short: A set of MySQL/MariaDB/Percona cluster topologies + short: A set of MySQL/MariaDB/Percona cluster topologies with pre-configured replication and auto-discovery of the nodes. logo: /images/mysql-mariadb-percona-logo.png?_r=1