├── .dockerignore ├── Dockerfile-10.11 ├── Dockerfile-10.5 ├── Dockerfile-11.4 ├── LICENSE ├── README.md ├── bin ├── galera-healthcheck └── qpress-11-linux-x64.tar ├── conf.d ├── 20-charset.cnf ├── 30-galera.cnf ├── 31-auth-socket.cnf └── mysqld_safe_syslog.cnf ├── examples ├── kontena │ ├── README.md │ └── kontena.yml ├── kubernetes │ ├── kustomize │ │ ├── README.md │ │ ├── galera-cluster.yml │ │ ├── galera-secrets.yml │ │ ├── galera-seed.yml │ │ ├── kustomization.yml │ │ ├── service.yml │ │ └── statefulset.yml │ └── others │ │ ├── README.md │ │ ├── deployment.yml │ │ ├── secrets.yml │ │ ├── service.yml │ │ └── statefulset.yml ├── nomad │ └── mariadb-galera.hcl ├── stack │ ├── README.md │ └── docker-compose.yml └── swarm │ ├── README.md │ └── docker-compose.yml ├── healthcheck.sh ├── makefile ├── mysqld.sh ├── no-galera-healthcheck.sh ├── notify.sh ├── primary-component.sql ├── run-upgrades.sh ├── start.sh └── test.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | LICENSE 2 | README.md 3 | examples 4 | makefile 5 | Dockerfile-* 6 | test.sh 7 | -------------------------------------------------------------------------------- /Dockerfile-10.11: -------------------------------------------------------------------------------- 1 | FROM mariadb:10.11 2 | 3 | # Download blocked from http://www.quicklz.com/qpress-11-linux-x64.tar 4 | COPY bin/qpress-11-linux-x64.tar /tmp/qpress.tar 5 | 6 | RUN set -x \ 7 | && apt-get update \ 8 | && apt-get install -y --no-install-recommends --no-install-suggests \ 9 | curl \ 10 | netcat \ 11 | pigz \ 12 | && tar -C /usr/local/bin -xf /tmp/qpress.tar qpress \ 13 | && chmod +x /usr/local/bin/qpress \ 14 | && rm -rf /tmp/* /var/cache/apk/* /var/lib/apt/lists/* 15 | 16 | COPY conf.d/* /etc/mysql/conf.d/ 17 | COPY *.sh /usr/local/bin/ 18 | COPY bin/galera-healthcheck /usr/local/bin/galera-healthcheck 19 | COPY primary-component.sql / 20 | 21 | RUN set -ex ;\ 22 | # Fix permissions 23 | chown -R mysql:mysql /etc/mysql ;\ 24 | chmod -R go-w /etc/mysql ;\ 25 | # Remove auth_socket config - already enabled in 10.4 by default 26 | rm /etc/mysql/conf.d/31-auth-socket.cnf ;\ 27 | # Disable code that deletes progress file after SST 28 | sed -i 's#-p \$progress#-p \$progress-XXX#' /usr/bin/wsrep_sst_mariabackup 29 | 30 | EXPOSE 3306 4444 4567 4567/udp 4568 8080 8081 31 | 32 | HEALTHCHECK CMD /usr/local/bin/healthcheck.sh 33 | 34 | ENV SST_METHOD=mariabackup 35 | 36 | ENTRYPOINT ["start.sh"] 37 | -------------------------------------------------------------------------------- /Dockerfile-10.5: -------------------------------------------------------------------------------- 1 | FROM mariadb:10.5 2 | 3 | # Download blocked from http://www.quicklz.com/qpress-11-linux-x64.tar 4 | COPY bin/qpress-11-linux-x64.tar /tmp/qpress.tar 5 | 6 | RUN set -x \ 7 | && apt-get update \ 8 | && apt-get install -y --no-install-recommends --no-install-suggests \ 9 | curl \ 10 | netcat \ 11 | pigz \ 12 | && tar -C /usr/local/bin -xf /tmp/qpress.tar qpress \ 13 | && chmod +x /usr/local/bin/qpress \ 14 | && rm -rf /tmp/* /var/cache/apk/* /var/lib/apt/lists/* 15 | 16 | COPY conf.d/* /etc/mysql/conf.d/ 17 | COPY *.sh /usr/local/bin/ 18 | COPY bin/galera-healthcheck /usr/local/bin/galera-healthcheck 19 | COPY primary-component.sql / 20 | 21 | RUN set -ex ;\ 22 | # Fix permissions 23 | chown -R mysql:mysql /etc/mysql ;\ 24 | chmod -R go-w /etc/mysql ;\ 25 | # Remove auth_socket config - already enabled in 10.4 by default 26 | rm /etc/mysql/conf.d/31-auth-socket.cnf ;\ 27 | # Disable code that deletes progress file after SST 28 | sed -i 's#-p \$progress#-p \$progress-XXX#' /usr/bin/wsrep_sst_mariabackup 29 | 30 | EXPOSE 3306 4444 4567 4567/udp 4568 8080 8081 31 | 32 | HEALTHCHECK CMD /usr/local/bin/healthcheck.sh 33 | 34 | ENV SST_METHOD=mariabackup 35 | 36 | ENTRYPOINT ["start.sh"] 37 | -------------------------------------------------------------------------------- /Dockerfile-11.4: -------------------------------------------------------------------------------- 1 | FROM mariadb:11.4 2 | 3 | RUN set -x \ 4 | && apt-get update \ 5 | && apt-get install -y --no-install-recommends --no-install-suggests \ 6 | curl \ 7 | netcat-traditional \ 8 | pigz \ 9 | && rm -rf /tmp/* /var/cache/apk/* /var/lib/apt/lists/* 10 | 11 | COPY conf.d/* /etc/mysql/conf.d/ 12 | COPY *.sh /usr/local/bin/ 13 | COPY bin/galera-healthcheck /usr/local/bin/galera-healthcheck 14 | COPY primary-component.sql / 15 | 16 | RUN set -ex ;\ 17 | # Fix permissions 18 | chown -R mysql:mysql /etc/mysql ;\ 19 | chmod -R go-w /etc/mysql ;\ 20 | # Remove auth_socket config - already enabled in 10.4 by default 21 | rm /etc/mysql/conf.d/31-auth-socket.cnf ;\ 22 | # Remove options that are no longer supported 23 | sed -i '/innodb_lock_schedule_algorithm/d' /etc/mysql/conf.d/30-galera.cnf ; 24 | 25 | 26 | EXPOSE 3306 4444 4567 4567/udp 4568 8080 8081 27 | 28 | HEALTHCHECK CMD /usr/local/bin/healthcheck.sh 29 | 30 | ENV SST_METHOD=mariabackup 31 | 32 | ENTRYPOINT ["start.sh"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MariaDb Galera Cluster 2 | 3 | This Docker container is based on the official Docker [mariadb](https://hub.docker.com/_/mariadb/) image and is designed to be 4 | compatible with auto-scheduling systems, specifically Kubernetes, Docker Swarm Mode and Kontena Classic. 5 | It could also work with manual scheduling (`docker run`) by specifying the correct 6 | environment variables or possibly other scheduling systems that use similar conventions. 7 | 8 | ## [Tags](https://hub.docker.com/r/colinmollenhour/mariadb-galera-swarm/tags) 9 | 10 | It is recommended to test all upgrades carefully! The `latest` tag should not be used for production! 11 | 12 | Several version are supported and rebuilt and tagged occasionally with both a version number and date built 13 | so just use the latest build for your major version that suits you or build your own version. Be sure to test 14 | replication functionality when building new versions using the `test.sh` script! 15 | 16 | ## How It Works 17 | 18 | This is not a simple config update, much effort has gone into making this container automate the initialization 19 | and *recovery* of a cluster. The entrypoint script orchestrates full recovery on simultaneous reset by having 20 | nodes communicate with each other *before* starting the mariadbd process to ensure that the cluster is recovered correctly. 21 | It does this by examining Galera's state files, recovering the GTID position on all nodes and then communicating this 22 | between nodes to find the most up-to-date one to form a new cluster if needed. It also provides multiple healthcheck 23 | endpoints for varying degress of healthiness to aid with integration of load balancers and scheduling systems. 24 | 25 | ### Examples 26 | 27 | - [Kubernetes](https://github.com/colinmollenhour/mariadb-galera-swarm/tree/master/examples/kubernetes) 28 | - [Docker Swarm](https://github.com/colinmollenhour/mariadb-galera-swarm/blob/master/examples/swarm) 29 | - [Kontena Classic](https://github.com/colinmollenhour/mariadb-galera-swarm/blob/master/examples/kontena) 30 | 31 | Please submit more examples for Mesos, etc. and also improvements for existing examples! 32 | 33 | By using DNS resolution to discover other nodes, they don't have to be specified explicitly so this container 34 | should work with any system with DNS-based service discovery such as Kubernetes, Docker Swarm, Consul, etc. 35 | 36 | ## Commands 37 | 38 | The entrypoint takes as a command one of the following startup "modes". Additional arguments will be passed to the `mariadbd` 39 | command line. For example, you will typically want to add `--log-bin=mysqld-bin` to enable binary logging. 40 | 41 | ### seed 42 | 43 | Used only to initialize a new cluster and after initialization and other nodes are joined 44 | the "seed" container should be stopped and replaced with a "node" container using the same volume. 45 | 46 | ### node 47 | 48 | Join an existing cluster. Takes as a second argument a comma-separated list of IPs or 49 | hostnames to resolve which are used to build the `--wsrep_cluster_address` option for joining a cluster. 50 | 51 | A "node" can actually also be used to bootstrap a cluster as the "seed" does described above by placing a flag 52 | file in the data volume before boot name `/var/lib/mysql/new-cluster`. 53 | 54 | ### no-galera 55 | 56 | Start server with Galera disabled. Useful for maintenance tasks like performing `mysql_upgrade` 57 | and resetting root credentials. 58 | 59 | #### Reset root password 60 | 61 | For example, to reset the root user password, with the Galera container stopped you can run a new temporary 62 | container like so: 63 | 64 | ``` 65 | shell1 $ docker run --rm -v {volume-name}:/var/lib/mysql --name no-galera-temp {image} no-galera --skip-grant-tables 66 | shell2 $ docker exec -it no-galera-temp mysql -u root mysql 67 | MariaDB> update user set password=password("YOUR_NEW_PASSWORD") where user='root' and host='127.0.0.1'; 68 | MariaDB> flush privileges; 69 | MariaDB> quit 70 | shell2 $ exit 71 | shell1 $ 72 | ``` 73 | 74 | And now start your Galera container back up with the new root password. 75 | 76 | ### sleep 77 | 78 | Start the container but not the server. Runs "sleep infinity". Useful just to get volumes 79 | initialized or if you want to `docker exec` without the server running. 80 | 81 | ### bash 82 | 83 | Open a bash shell instead of starting the container. For example `bash -c 'touch /var/lib/mysql/new-cluster'` to touch the 84 | flag file that indicates to start a new cluster on the next startup. 85 | 86 | ## Environment Variables 87 | 88 | - `XTRABACKUP_PASSWORD` (required unless `XTRABACKUP_PASSWORD_FILE` is provided) 89 | - `SYSTEM_PASSWORD` (required or set to a hash of `XTRABACKUP_PASSWORD` if provided.) 90 | - `CLUSTER_NAME` (optional) 91 | - `NODE_ADDRESS` (optional - defaults to ethwe, then eth0) 92 | - `LISTEN_WHEN_HEALTHY` (optional) - Specify a port number to open a healthcheck socket on once the cluster 93 | has reached a healthy state. Useful with Kontena's `wait_for_port` feature. 94 | - `HEALTHY_WHILE_BOOTING` (optional) - If '1' then the HEALTHCHECK script will report healthy 95 | during the boot phase (waiting for DNS to resolve and recovering wsrep position). 96 | - `SKIP_TZINFO` (optional) - Specify any value to skip loading of timezone table data when initing a new directory. 97 | - `DEFAULT_TIME_ZONE` (optional - defaults to the `TZ` envvar or to '+00:00' if undefined) - Specify the database's time zone, either in numeric format (+01:00) or in verbal format (CET, Europe/Bratislava, etc.). The latter one is possible only if you haven't specified `SKIP_TZINFO`. More information about why you would need this is [here](https://mariadb.com/kb/en/library/time-zones/). 98 | - `SST_METHOD` (optional - defaults to 'mariabackup' for 10.2+ and 'xtrabackup-v2' for 10.1) May also be set to 'rsync' or 'mysqldump'. Other methods requiring further configuration or installed dependencies are not available in this image. 99 | - `SKIP_UPGRADES` (optional - prevent running `run-upgrades.sh` script) 100 | 101 | Additional variables for "seed": 102 | 103 | - `MYSQL_ROOT_PASSWORD` (optional) - See also `/var/lib/mysql/new-cluster` flag file. 104 | - `MYSQL_ROOT_HOST` (optional) - Defaults to '127.0.0.1' if not specified. Specify '%' to allow root login from any host. 105 | - `MYSQL_ROOT_SOCKET_AUTH` (optional) - Enabled by default, specify `0` to disable. If enabled `'root'@'localhost'` is created on bootstrap such that 106 | root can login via the unix socket without a password! This allows `docker exec` commands to work without a password while still requiring a password for 107 | login over the network. 108 | - `MYSQL_DATABASE` (optional) 109 | - `MYSQL_USER` (optional) 110 | - `MYSQL_PASSWORD` (optional) 111 | 112 | Additional variables for "node": 113 | 114 | - `GCOMM_MINIMUM` (optional - defaults to 2) - Set this to the minimum number of nodes required to start the cluster without waiting. For example, use `GCOMM_MINIMUM=3` for a 3-node cluster. 115 | 116 | ### Providing secrets through files 117 | 118 | It's also possible to configure the sensitive variables using files, a method used by [Docker Swarm](https://docs.docker.com/engine/swarm/secrets/), 119 | Rancher and perhaps others. The paths to the secret files defaults to `/run/secrets/{lower_case_variable_name}` 120 | but can be specified explicitly as well using the following environment variables: 121 | 122 | - `XTRABACKUP_PASSWORD_FILE` 123 | - `SYSTEM_PASSWORD_FILE` 124 | - `MYSQL_ROOT_PASSWORD_FILE` 125 | - `MYSQL_ROOT_HOST_FILE` 126 | - `MYSQL_PASSWORD_FILE` 127 | - `MYSQL_DATABASE_FILE` 128 | 129 | ## Flag Files 130 | 131 | In order to accomodate controlling the bootstrapping phase without having to change the CMD which is sometimes 132 | hard to do with automated schedulers you can touch the following files to change the bootstrapping behavior 133 | before starting the container. All files are expected to be in the /var/lib/mysql directory which you should be 134 | mounting as a container volume. 135 | 136 | - `/var/lib/mysql/new-cluster` - Cause a 'node' container to behave as a 'seed' container on it's first run. This 137 | may also be used for recovery in case a Primary Component cannot be formed or for bootstrapping a fresh cluster 138 | in place of using the 'seed' container. If the file has any contents they will be used as the `MYSQL_ROOT_PASSWORD`. 139 | - `/var/lib/mysql/hold-start` - Cause a 'node' container to wait until this file is deleted before trying to boot. 140 | This could be used in the absence of a scheduler with an easy fine-grained scheduling control. 141 | - `/var/lib/mysql/force-cluster-bootstrapping` - Force the creation of MySQL users again ('seed' or 'node' command). 142 | - `/var/lib/mysql/skip-cluster-bootstrapping` - Prevent the creation of MySQL users. This file will be created and 143 | **should not** be deleted under normal circumstances. 144 | - `/var/lib/mysql/skip-upgrades` - Prevent running the `run-upgrades.sh` script. 145 | 146 | ## Health Checks 147 | 148 | By default there are two HTTP-based healthcheck servers running in the background. 149 | 150 | - Port 8080 only reports healthy when ready to serve clients. (synced) 151 | - Port 8081 reports healthy as long as the server is synced or donor/desynced state. This one is used to help 152 | other nodes determine cluster state before launching the server and also by the Dockerfile HEALTHCHECK command. 153 | 154 | The default `HEALTHCHECK` command also returns healthy status if `/var/lib/mysql/sst_in_progress` is present to avoid 155 | a node being killed during an SST. Otherwise it uses the second health check (port 8081) to return healthy only if it 156 | is 'synced' to prevent the node from being killed if it is a donor for a long period of time. How you want the 157 | healthcheck command to behave will vary on your uses for the healthcheck so you may need to override it depending on 158 | the behavior you desire. Regardless, both healthcheck servers will be started and will use negligible resources unless 159 | they are actually being pinged. 160 | 161 | Additionally, if a `LISTEN_WHEN_HEALTHY` port number is specified then the container will start a loop checking it's 162 | own port 8080 health check described above until it reports healthy at which point it will open a new socket on this 163 | port which just forwards to port 8080. This can be used with Kontena's `wait_for_port` feature to accomodate the 164 | rolling update mechanism. 165 | 166 | ## Upgrading 167 | 168 | In general, since MariaDb is not a particularly stable server, often introducing regressions or backwards-compatibility 169 | breaks with minor version numbers it is always advised to use specific tags in production and test upgrades on a staging 170 | environment first. Also it is advised to read the commit log to see what changes have been made. No warranty is provided, 171 | use at your own risk! 172 | 173 | ### Upgrading from 10.1 to 10.2 174 | 175 | Before upgrading you need to grant the `PROCESS` privilege to the xtrabackup user: 176 | 177 | mysql> GRANT PROCESS ON *.* TO 'xtrabackup'@'localhost'; 178 | 179 | ## Running Tests 180 | 181 | The `test.sh` script is a simple script that starts the seed and node containers using `docker run`. It starts the seed 182 | in the background, outputs the seed logs to the console, then starts a node in the foreground. Success of the test 183 | should be determined by inspecting the logs and additional actions like starting additional nodes and killing one or more 184 | nodes should be performed to verify that recovery works as expected. 185 | 186 | **Example** 187 | 188 | ``` 189 | make test-10.11 190 | ``` 191 | 192 | Or build and run your own image for 10.11 and start a single seed and node: 193 | 194 | ``` 195 | $ docker build . -f Dockerfile-10.11 -t galera-test-10.11 196 | $ ./test.sh galera-test-10.11 197 | ``` 198 | 199 | In another console, verify the cluster status and size: 200 | 201 | ``` 202 | $ docker exec -it cm-galera-test-node1 mysql -be "show status like 'wsrep_cluster_s%';" 203 | +----------------------------+--------------------------------------+ 204 | | Variable_name | Value | 205 | +----------------------------+--------------------------------------+ 206 | | wsrep_cluster_size | 3 | 207 | | wsrep_cluster_state_uuid | e54090dc-f892-11ec-ad6f-f2704a681293 | 208 | | wsrep_cluster_status | Primary | 209 | +----------------------------+--------------------------------------+ 210 | ``` 211 | 212 | # More Info 213 | 214 | - Tries to handle as many recovery scenarios as possible including full cluster ungraceful shutdown by 215 | using `--wsrep-recovery` and inter-node communication to discover the optimal node for bootstrapping 216 | a new cluster when the old one cannot be recovered. 217 | - If you need to perform manual recovery of a previously healthy cluster you can use "node" mode 218 | but touch a file at `/var/lib/mysql/new-cluster` to force a node to bootstrap a new cluster 219 | and bypass the automatic recovery steps. 220 | - XtraBackup is used for state transfer and MariaDb now supports `pc.recovery` so the primary component should 221 | automatically be recovered in the case of all nodes being gracefully shutdown. It is important tha all nodes are 222 | started together so that they can communicate status with each-other. 223 | - A go server runs within the cluster exposing an http service for intelligent health checking. 224 | - Port 8080 is used by the Docker 1.12 HEALTHCHECK feature and also can be used by any other health checking 225 | node in the network such as HAProxy or Consul to determine readable/writeable nodes. 226 | - Port 8081 is used to detemine cluster status 227 | - If your container network uses something other than `ethwe*` or `eth0` then you need to specify `NODE_ADDRESS` 228 | as either the name of the interface to listen on or a grep pattern to match one of the container's IP addresses. 229 | E.g.: `NODE_ADDRESS='^10.0.1.*'` 230 | - When using DNS for node address discovery the container entrypoint script will wait indefinitely for 231 | `GCOMM_MINIMUM` IP addresses to resolve before trying to start `mysqld` in case some containers are starting 232 | slower than others to increase the chance of a healthy recovery. Scenarios where not enough IPs would resolve 233 | might include: 234 | - Some nodes may finish pulling container images from remote repositories sooner than others 235 | - Schedulers may not be launching nodes quickly enough 236 | - Service discovery systems may be slow to propagate updates via DNS 237 | - If the file `/usr/local/lib/startup.sh` exists it will be sourced in the `start.sh` script allowing you to perform 238 | initialization tasks if needed. 239 | - If you need to promote a running node to be a new "Primary Component" you can run the following command to do so: 240 | - `docker exec -i mysql -p /primary-component.sql` 241 | - You can monitor cluster state changes more clearly by setting `wsrep_notify_cmd` to `/usr/local/bin/notify.sh` 242 | which will output the updates to the Docker logs/console or use your own command to report to your monitoring systems. 243 | 244 | # Credit 245 | 246 | - Forked from ["jakolehm/docker-galera-mariadb-10.0"](https://github.com/jakolehm/docker-galera-mariadb-10.0) 247 | - Forked from ["sttts/docker-galera-mariadb-10.0"](https://github.com/sttts/docker-galera-mariadb-10.0) 248 | - galera-healthcheck go binary from ["sttts/galera-healthcheck"](https://github.com/sttts/galera-healthcheck) 249 | -------------------------------------------------------------------------------- /bin/galera-healthcheck: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinmollenhour/mariadb-galera-swarm/28cb4608c25d1b2ce2b4cc09533ec44043d6da99/bin/galera-healthcheck -------------------------------------------------------------------------------- /bin/qpress-11-linux-x64.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinmollenhour/mariadb-galera-swarm/28cb4608c25d1b2ce2b4cc09533ec44043d6da99/bin/qpress-11-linux-x64.tar -------------------------------------------------------------------------------- /conf.d/20-charset.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | default-character-set = utf8mb4 3 | 4 | [mysql] 5 | default-character-set = utf8mb4 6 | 7 | [mysqld] 8 | collation-server = utf8mb4_unicode_ci 9 | init-connect = 'SET NAMES utf8mb4' 10 | character-set-server = utf8mb4 11 | -------------------------------------------------------------------------------- /conf.d/30-galera.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | bind-address=0.0.0.0 3 | binlog_format=ROW 4 | default-storage-engine=innodb 5 | 6 | # Best practice for Galera 7 | innodb_autoinc_lock_mode=2 8 | innodb_lock_schedule_algorithm=FCFS 9 | 10 | # Subsequent `wsrep_provider_options` will replace the ones specified below 11 | wsrep_provider=/usr/lib/galera/libgalera_smm.so 12 | wsrep_provider_options="gcache.size=2048M; gcache.keep_pages_size=1024M; gcache.recover=yes;" 13 | #wsrep_sst_method=xtrabackup-v2 # This is set with SST_METHOD env variable 14 | #wsrep_slave_threads=4 15 | 16 | # 17 | # Options for xtrabackup-v2 18 | # 19 | [sst] 20 | sst-syslog=-1 21 | progress=/tmp/mysql-console/fifo 22 | #inno-apply-opts="--use-memory=2G" 23 | #compressor="pigz --fast --processes 4" 24 | #decompressor="pigz --decompress" 25 | -------------------------------------------------------------------------------- /conf.d/31-auth-socket.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | 3 | # Required for run-upgrades.sh to work 4 | plugin-load-add=auth_socket.so 5 | 6 | -------------------------------------------------------------------------------- /conf.d/mysqld_safe_syslog.cnf: -------------------------------------------------------------------------------- 1 | # This file is here to overwrite the file added by the core package to revert the options below 2 | [mysqld_safe] 3 | #skip_log_error 4 | #syslog 5 | 6 | -------------------------------------------------------------------------------- /examples/kontena/README.md: -------------------------------------------------------------------------------- 1 | MariaDb Galera Cluster on Kontena in 2 steps 2 | -------------------------------------------- 3 | 4 | If you need to import a large database, uncomment the "hold-start" hook so that you can load the data 5 | on the seed node and then remove the hold-start on each node to sync. 6 | 7 | $ kontena volume create --driver local --scope instance galera-data 8 | $ kontena stack install -n galera kontena.yml 9 | 10 | Now import your database dump using your preferred method. The root password can be found in the docker 11 | logs of the seed node. 12 | 13 | Remove the hold-start flag if you uncommented the "hold-start" hook: 14 | 15 | $ kontena service exec --instance 2 galera/node rm /var/lib/mysql/hold-start 16 | $ kontena service exec --instance 3 galera/node rm /var/lib/mysql/hold-start 17 | -------------------------------------------------------------------------------- /examples/kontena/kontena.yml: -------------------------------------------------------------------------------- 1 | # 2 | # MariaDB Galera database services 3 | # 4 | --- 5 | stack: galera 6 | version: 0.5.6 7 | description: MariaDB Galera Cluster 8 | expose: node 9 | 10 | variables: 11 | 12 | galera_xtrabackup_password: 13 | type: string 14 | from: 15 | vault: GALERA_XTRABACKUP_PASSWORD 16 | random_string: 32 17 | to: 18 | vault: GALERA_XTRABACKUP_PASSWORD 19 | 20 | services: 21 | 22 | node: 23 | image: colinmollenhour/mariadb-galera-swarm 24 | environment: 25 | - GCOMM_MINIMUM=3 26 | - LISTEN_WHEN_HEALTHY=8082 27 | secrets: 28 | - secret: GALERA_XTRABACKUP_PASSWORD 29 | name: XTRABACKUP_PASSWORD 30 | type: env 31 | command: "node node" 32 | stateful: true 33 | volumes: 34 | - mariadb-data:/var/lib/mysql 35 | deploy: 36 | strategy: daemon 37 | min_health: 0.6 38 | wait_for_port: 8082 39 | instances: 1 40 | hooks: 41 | pre_start: 42 | # - name: node 43 | # cmd: touch /var/lib/mysql/hold-start 44 | # instances: "*" 45 | # oneshot: true 46 | - name: seed 47 | cmd: rm -f /var/lib/mysql/hold-start; touch /var/lib/mysql/new-cluster; chmod 666 /var/lib/mysql/new-cluster 48 | instances: 1 49 | oneshot: true 50 | 51 | volumes: 52 | mariadb-data: 53 | external: 54 | name: mariadb-data 55 | 56 | -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/README.md: -------------------------------------------------------------------------------- 1 | 2 | Kubernetes 3 | ========== 4 | 5 | I could not find a working galera or multi master helm chart or kustomize packaging. Stumbled upon this image with a working docker-compose example. And decided to get it running in kubernetes. Am sharing this to say thanks to 6 | colinmollenhour who clearly put a lots of effort in this image. I will create a helm chart from this 7 | configuration later. If you have any improvements please submit them. 8 | 9 | 1. Generate XTRABACKUP_PASSWORD, SYSTEM_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD 10 | and put them in secrets.yml 11 | 12 | ``` 13 | $ openssl rand -base64 32 | base64 14 | ``` 15 | 16 | 2. Verify if statefullset arg matches seed service dns name: seed,mariadb-galera-seed 17 | 18 | 3. Setup the cluster 19 | 20 | ``` 21 | $ kubectl apply -k kustomize/ 22 | ``` -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/galera-cluster.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: galera-cluster -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/galera-secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: galera-secrets 5 | namespace: galera-cluster 6 | type: Opaque 7 | stringData: 8 | system_password: "password" # Use your own 9 | db_name: "wordpressDB" # Use your own 10 | db_user: "wordpress" # Use your own 11 | db_password: "wordPressPassword" # Use your own 12 | db_root_password: "myrootpassword" # Use your own 13 | xtrabackup_password: "password" # Use your own -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/galera-seed.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mariadb-galera-seed 5 | namespace: galera-cluster 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: mariadb-galera-seed 11 | template: 12 | metadata: 13 | labels: 14 | app: mariadb-galera-seed 15 | spec: 16 | volumes: 17 | - name: mysql-data 18 | emptyDir: {} 19 | containers: 20 | - name: 'mariadb-galera-seed' 21 | image: colinmollenhour/mariadb-galera-swarm:latest 22 | args: 23 | - seed 24 | ports: 25 | - containerPort: 3306 26 | name: mysql 27 | - containerPort: 3309 28 | name: recovery 29 | - containerPort: 4444 30 | name: sst 31 | - containerPort: 4567 32 | name: gcom 33 | - containerPort: 4568 34 | name: gcom2 35 | - containerPort: 8080 36 | name: hup 37 | - containerPort: 8081 38 | name: hboot 39 | env: 40 | - name: CLUSTER_NAME 41 | value: "galera-cluster" 42 | - name: DEFAULT_TIME_ZONE 43 | value: "+01:00" 44 | - name: NODE_ADDRESS 45 | value: "^10.42.*.*" 46 | - name: SST_METHOD 47 | value: "rsync" 48 | - name: MYSQL_ROOT_HOST 49 | value: "%" 50 | - name: MYSQL_ROOT_SOCKET_AUTH 51 | value: "0" 52 | - name: MYSQL_DATABASE 53 | valueFrom: 54 | secretKeyRef: 55 | key: db_name 56 | name: galera-secrets 57 | - name: MYSQL_PASSWORD 58 | valueFrom: 59 | secretKeyRef: 60 | name: galera-secrets 61 | key: db_password 62 | - name: MYSQL_ROOT_PASSWORD 63 | valueFrom: 64 | secretKeyRef: 65 | key: db_root_password 66 | name: galera-secrets 67 | - name: MYSQL_USER 68 | valueFrom: 69 | secretKeyRef: 70 | key: db_user 71 | name: galera-secrets 72 | - name: XTRABACKUP_PASSWORD 73 | valueFrom: 74 | secretKeyRef: 75 | key: xtrabackup_password 76 | name: galera-secrets 77 | - name: SYSTEM_PASSWORD 78 | valueFrom: 79 | secretKeyRef: 80 | key: system_password 81 | name: galera-secrets -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/kustomization.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - galera-cluster.yml 5 | - galera-secrets.yml 6 | - service.yml 7 | - galera-seed.yml 8 | - statefulset.yml 9 | 10 | # patches: 11 | # - 12 | 13 | # images: 14 | # - 15 | 16 | -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: mariadb-galera 5 | namespace: galera-cluster 6 | labels: 7 | app: mariadb-galera 8 | spec: 9 | ports: 10 | - port: 3306 11 | name: mysql 12 | - port: 3309 13 | name: recovery 14 | type: NodePort 15 | # Keep session bound to specific node 16 | sessionAffinity: ClientIP 17 | # Get real client IP for session affinity usage 18 | externalTrafficPolicy: Local 19 | selector: 20 | app: mariadb-galera-node 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | name: mariadb-galera-seed 26 | namespace: galera-cluster 27 | labels: 28 | app: mariadb-galera 29 | spec: 30 | ports: 31 | - port: 3306 32 | name: mysql 33 | - port: 3309 34 | name: recovery 35 | - port: 4444 36 | name: sst 37 | - port: 4567 38 | name: gcom 39 | - port: 4568 40 | name: gcom2 41 | - port: 8080 42 | name: hup 43 | - port: 8081 44 | name: hboot 45 | type: ClusterIP 46 | selector: 47 | app: mariadb-galera-seed -------------------------------------------------------------------------------- /examples/kubernetes/kustomize/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: mariadb-galera 5 | namespace: galera-cluster 6 | spec: 7 | serviceName: "mariadb-galera" 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: mariadb-galera-node 12 | template: 13 | metadata: 14 | labels: 15 | app: mariadb-galera-node 16 | spec: 17 | initContainers: 18 | # Init container only required to rm lost+found directory on mounted volume 19 | - name: cleanup 20 | image: colinmollenhour/mariadb-galera-swarm 21 | command: [ "/bin/bash", "-c", "--" ] 22 | args: ["rm -rf /var/lib/mysql/lost+found"] 23 | volumeMounts: 24 | - name: mysql-data 25 | mountPath: /var/lib/mysql 26 | containers: 27 | - name: mariadb-galera 28 | image: colinmollenhour/mariadb-galera-swarm:latest 29 | env: 30 | - name: CLUSTER_NAME 31 | value: "galera-cluster" 32 | - name: LISTEN_WHEN_HEALTHY 33 | value: "8082" 34 | - name: HEALTHY_WHILE_BOOTING 35 | value: "1" 36 | - name: DEFAULT_TIME_ZONE 37 | value: "+01:00" 38 | - name: NODE_ADDRESS 39 | value: "^10.42.*.*" 40 | - name: SST_METHOD 41 | value: "rsync" 42 | - name: MYSQL_ROOT_SOCKET_AUTH 43 | value: "0" 44 | - name: MYSQL_PASSWORD 45 | valueFrom: 46 | secretKeyRef: 47 | key: db_password 48 | name: galera-secrets 49 | - name: MYSQL_ROOT_PASSWORD 50 | valueFrom: 51 | secretKeyRef: 52 | key: db_root_password 53 | name: galera-secrets 54 | - name: MYSQL_USER 55 | valueFrom: 56 | secretKeyRef: 57 | key: db_user 58 | name: galera-secrets 59 | - name: XTRABACKUP_PASSWORD 60 | valueFrom: 61 | secretKeyRef: 62 | key: xtrabackup_password 63 | name: galera-secrets 64 | - name: SYSTEM_PASSWORD 65 | valueFrom: 66 | secretKeyRef: 67 | key: system_password 68 | name: galera-secrets 69 | resources: 70 | requests: 71 | memory: "2Gi" 72 | cpu: "2" 73 | limits: 74 | memory: "5Gi" 75 | cpu: "2" 76 | ports: 77 | - containerPort: 3306 78 | name: mysql 79 | - containerPort: 3309 80 | name: recovery 81 | - containerPort: 4444 82 | name: sst 83 | - containerPort: 4567 84 | name: gcom 85 | - containerPort: 4568 86 | name: gcom2 87 | - containerPort: 8080 88 | name: hup 89 | - containerPort: 8081 90 | name: hboot 91 | args: 92 | - node 93 | - seed,mariadb-galera-seed 94 | readinessProbe: 95 | tcpSocket: 96 | port: 8080 97 | initialDelaySeconds: 15 98 | timeoutSeconds: 5 99 | successThreshold: 2 100 | livenessProbe: 101 | tcpSocket: 102 | port: 8081 103 | initialDelaySeconds: 60 104 | periodSeconds: 15 105 | volumeMounts: 106 | - name: mysql-data 107 | mountPath: /var/lib/mysql 108 | volumeClaimTemplates: 109 | - metadata: 110 | name: mysql-data 111 | spec: 112 | accessModes: [ "ReadWriteOnce" ] 113 | # Use anothe storage class for your specific usage 114 | storageClassName: rook-ceph-block 115 | resources: 116 | requests: 117 | storage: 20Gi -------------------------------------------------------------------------------- /examples/kubernetes/others/README.md: -------------------------------------------------------------------------------- 1 | Kubernetes 2 | ========== 3 | I could not find a working galera or multi master helm chart. Stumbled upon this image with a working 4 | docker-compose example. And decided to get it running in kubernetes. Am sharing this to say thanks to 5 | colinmollenhour who clearly put a lots of effort in this image. I will create a helm chart from this 6 | configuration later. If you have any improvements please submit them. 7 | 8 | 9 | 1. Generate XTRABACKUP_PASSWORD, SYSTEM_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD 10 | and put them in secrets.yml 11 | 12 | ``` 13 | $ openssl rand -base64 32 | base64 14 | 15 | ``` 16 | 17 | 2. Verify if statefullset arg matches seed service dns name: seed,mariadb-galera-seed.default 18 | 19 | 3. Setup the cluster 20 | ``` 21 | $ kubectl create -f secrets.yml && \ 22 | kubectl create -f service.yml && \ 23 | kubectl create -f deployment.yml && \ 24 | kubectl create -f statefullset.yml 25 | ``` 26 | 27 | 28 | Note! Am using the _FILE env variables because had issues with using the direct variables in 29 | combination with kubernetes secrets. -------------------------------------------------------------------------------- /examples/kubernetes/others/deployment.yml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | metadata: 3 | name: mariadb-galera-seed 4 | namespace: default 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: mariadb-galera-seed 11 | spec: 12 | volumes: 13 | - name: galera-secrets 14 | secret: 15 | secretName: mariadb-galera 16 | - name: mysql-data 17 | emptyDir: {} 18 | containers: 19 | - name: 'mariadb-galera-seed' 20 | image: colinmollenhour/mariadb-galera-swarm 21 | args: 22 | - seed 23 | ports: 24 | - containerPort: 3306 25 | name: mysql 26 | - containerPort: 3309 27 | name: recovery 28 | - containerPort: 4444 29 | name: sst 30 | - containerPort: 4567 31 | name: gcom 32 | - containerPort: 4568 33 | name: gcom2 34 | - containerPort: 8080 35 | name: hup 36 | - containerPort: 8081 37 | name: hboot 38 | env: 39 | - name: CLUSTER_NAME 40 | value: "my-galera-cluster" 41 | - name: DEFAULT_TIME_ZONE 42 | value: "+01:00" 43 | - name: NODE_ADDRESS 44 | value: "^10.233.*.*" 45 | - name: SST_METHOD 46 | value: "rsync" 47 | - name: MYSQL_ROOT_HOST 48 | value: "%" 49 | - name: MYSQL_ROOT_SOCKET_AUTH 50 | value: "0" 51 | - name: MYSQL_DATABASE 52 | value: portal 53 | - name: MYSQL_PASSWORD_FILE 54 | value: /etc/secrets/MYSQL_PASSWORD 55 | - name: MYSQL_ROOT_PASSWORD_FILE 56 | value: /etc/secrets/MYSQL_ROOT_PASSWORD 57 | - name: MYSQL_USER 58 | value: user 59 | - name: XTRABACKUP_PASSWORD_FILE 60 | value: /etc/secrets/XTRABACKUP_PASSWORD 61 | - name: SYSTEM_PASSWORD_FILE 62 | value: /etc/secrets/SYSTEM_PASSWORD 63 | volumeMounts: 64 | - name: galera-secrets 65 | mountPath: "/etc/secrets" 66 | readOnly: true -------------------------------------------------------------------------------- /examples/kubernetes/others/secrets.yml: -------------------------------------------------------------------------------- 1 | kind: Secret 2 | metadata: 3 | name: mariadb-galera 4 | namespace: default 5 | type: Opaque 6 | data: 7 | XTRABACKUP_PASSWORD: 8 | SYSTEM_PASSWORD: 9 | MYSQL_ROOT_PASSWORD: 10 | MYSQL_PASSWORD: -------------------------------------------------------------------------------- /examples/kubernetes/others/service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | metadata: 3 | name: mariadb-galera 4 | namespace: default 5 | labels: 6 | app: mariadb-galera 7 | spec: 8 | ports: 9 | - port: 3306 10 | name: mysql 11 | - port: 3309 12 | name: recovery 13 | type: LoadBalancer 14 | # Keep session bound to specific node 15 | sessionAffinity: ClientIP 16 | # Get real client IP for session affinity usage 17 | externalTrafficPolicy: Local 18 | selector: 19 | app: mariadb-galera-node 20 | --- 21 | kind: Service 22 | metadata: 23 | name: mariadb-galera-seed 24 | namespace: default 25 | labels: 26 | app: mariadb-galera 27 | spec: 28 | ports: 29 | - port: 3306 30 | name: mysql 31 | - port: 3309 32 | name: recovery 33 | - port: 4444 34 | name: sst 35 | - port: 4567 36 | name: gcom 37 | - port: 4568 38 | name: gcom2 39 | - port: 8080 40 | name: hup 41 | - port: 8081 42 | name: hboot 43 | type: ClusterIP 44 | selector: 45 | app: mariadb-galera-seed -------------------------------------------------------------------------------- /examples/kubernetes/others/statefulset.yml: -------------------------------------------------------------------------------- 1 | kind: StatefulSet 2 | metadata: 3 | name: mariadb-galera 4 | namespace: default 5 | spec: 6 | serviceName: "mariadb-galera" 7 | replicas: 3 8 | selector: 9 | matchLabels: 10 | app: mariadb-galera-node 11 | template: 12 | metadata: 13 | labels: 14 | app: mariadb-galera-node 15 | spec: 16 | volumes: 17 | - name: galera-secrets 18 | secret: 19 | secretName: mariadb-galera 20 | initContainers: 21 | # Init container only required to rm lost+found directory on mounted volume 22 | - name: cleanup 23 | image: colinmollenhour/mariadb-galera-swarm 24 | command: [ "/bin/bash", "-c", "--" ] 25 | args: ["rm -rf /var/lib/mysql/lost+found"] 26 | volumeMounts: 27 | - name: datadir 28 | mountPath: /var/lib/mysql 29 | containers: 30 | - name: mariadb-galera 31 | image: colinmollenhour/mariadb-galera-swarm 32 | env: 33 | - name: CLUSTER_NAME 34 | value: "my-galera-cluster" 35 | - name: LISTEN_WHEN_HEALTHY 36 | value: "8082" 37 | - name: HEALTHY_WHILE_BOOTING 38 | value: "1" 39 | - name: DEFAULT_TIME_ZONE 40 | value: "+01:00" 41 | - name: NODE_ADDRESS 42 | value: "^10.233.*.*" 43 | - name: SST_METHOD 44 | value: "rsync" 45 | - name: MYSQL_ROOT_SOCKET_AUTH 46 | value: "0" 47 | - name: MYSQL_PASSWORD_FILE 48 | value: /etc/secrets/MYSQL_PASSWORD 49 | - name: MYSQL_ROOT_PASSWORD_FILE 50 | value: /etc/secrets/MYSQL_ROOT_PASSWORD 51 | - name: MYSQL_USER 52 | value: user 53 | - name: XTRABACKUP_PASSWORD_FILE 54 | value: /etc/secrets/XTRABACKUP_PASSWORD 55 | - name: SYSTEM_PASSWORD_FILE 56 | value: /etc/secrets/SYSTEM_PASSWORD 57 | ports: 58 | - containerPort: 3306 59 | name: mysql 60 | - containerPort: 3309 61 | name: recovery 62 | - containerPort: 4444 63 | name: sst 64 | - containerPort: 4567 65 | name: gcom 66 | - containerPort: 4568 67 | name: gcom2 68 | - containerPort: 8080 69 | name: hup 70 | - containerPort: 8081 71 | name: hboot 72 | args: 73 | - node 74 | - seed,mariadb-galera-seed.default 75 | readinessProbe: 76 | tcpSocket: 77 | port: 8080 78 | initialDelaySeconds: 15 79 | timeoutSeconds: 5 80 | successThreshold: 2 81 | livenessProbe: 82 | tcpSocket: 83 | port: 8081 84 | initialDelaySeconds: 60 85 | periodSeconds: 15 86 | volumeMounts: 87 | - name: datadir 88 | mountPath: /var/lib/mysql 89 | - name: galera-secrets 90 | mountPath: "/etc/secrets" 91 | readOnly: true 92 | volumeClaimTemplates: 93 | - metadata: 94 | name: datadir 95 | spec: 96 | accessModes: [ "ReadWriteOnce" ] 97 | storageClassName: "openebs-cstor-ssd" 98 | resources: 99 | requests: 100 | storage: 20Gi -------------------------------------------------------------------------------- /examples/nomad/mariadb-galera.hcl: -------------------------------------------------------------------------------- 1 | # Ports require lots of reconfiguration.. 2 | # https://mariadb.com/kb/en/configuring-mariadb-galera-cluster/#network-ports 3 | 4 | # Stuck on WSL-related issue: 5 | # failed to setup alloc: pre-run hook "network" failed: failed to configure networking for alloc: failed to configure network: plugin type="loopback" failed (add): failed to Statfs "/var/run/docker/netns/8a3dd4a21ef8": no such file or directory 6 | 7 | job "mariadb" { 8 | type = "service" 9 | 10 | group "node" { 11 | count = 3 12 | network { 13 | mode = "bridge" 14 | port "server" { 15 | static = 3306 16 | to = 3306 17 | } 18 | port "recovery" { 19 | static = 3309 20 | to = 3309 21 | } 22 | port "sst" { 23 | static = 4444 24 | to = 4444 25 | } 26 | port "gcomm" { 27 | static = 4567 28 | to = 4567 29 | } 30 | port "gcomm2" { 31 | static = 4568 32 | to = 4568 33 | } 34 | port "node-health" { 35 | static = 8080 36 | to = 8080 37 | } 38 | port "cluster-health" { 39 | static = 8081 40 | to = 8081 41 | } 42 | } 43 | 44 | service { 45 | name = "mariadb-server" 46 | port = "server" 47 | provider = "nomad" 48 | } 49 | 50 | task "mariadb-server" { 51 | driver = "docker" 52 | config { 53 | image = "colinmollenhour/mariadb-galera-swarm:10.11.6-2023-12-09" 54 | ports = ["server","recovery","sst","gcomm","gcomm2","node-health","cluster-health"] 55 | command = "node" 56 | args = ["??"] 57 | volumes = [ 58 | "data:/var/lib/mysql" 59 | ] 60 | } 61 | env { 62 | NODE_ADDRESS = "${NOMAD_ADDR_gcomm}" 63 | XTRABACKUP_PASSWORD = "foobar" 64 | SKIP_TZINFO = 1 65 | GCOMM_MINIMUM = 1 66 | } 67 | service { 68 | name = "mariadb-gcomm" 69 | port = "gcomm" 70 | provider = "nomad" 71 | } 72 | template { 73 | data = < 19.03 5 | https://github.com/moby/moby/pull/39204 6 | 7 | ``` 8 | $ docker network create -d overlay --attachable haproxy 9 | $ docker network create -d overlay --attachable galera 10 | $ docker stack deploy --compose-file docker-compose.yml galera 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /examples/stack/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | node: 5 | image: virtpanel/mariadb-galera-swarm 6 | command: node tasks.galera-seed,galera_node 7 | deploy: 8 | replicas: 4 9 | hostname: "{{.Service.Name}}.{{.Task.Slot}}" 10 | networks: 11 | - galera 12 | - haproxy 13 | volumes: 14 | - type: bind 15 | source: /d/galera/data 16 | target: /var/lib/mysql 17 | - type: bind 18 | source: /mnt/gfs/galera/secrets 19 | target: /run/secrets 20 | read_only: true 21 | 22 | networks: 23 | galera: 24 | driver: overlay 25 | external: true 26 | haproxy: 27 | driver: overlay 28 | external: true 29 | 30 | -------------------------------------------------------------------------------- /examples/swarm/README.md: -------------------------------------------------------------------------------- 1 | Docker Swarm 2 | ============ 3 | 4 | This is an example only and may not be production-quality. Please submit improvements! 5 | 6 | ``` 7 | $ mkdir -p .secrets 8 | $ openssl rand -base64 32 > .secrets/xtrabackup_password 9 | $ openssl rand -base64 32 > .secrets/mysql_password 10 | $ openssl rand -base64 32 > .secrets/mysql_root_password 11 | $ docker stack deploy -c docker-compose.yml galera 12 | $ docker service ls 13 | (wait for `galera_seed` to be healthy) 14 | $ docker service scale galera_node=2 15 | (wait for both `galera_node` instances to be healthy) 16 | $ docker service scale galera_seed=0 17 | $ docker service scale galera_node=3 18 | ``` 19 | 20 | The example `docker-compose.yml` file contains a user network called `galera_network`. You may want to use a different network 21 | that is shared with other components of your app or multiple networks. Just note that the `NODE_ADDRESS` pattern must be able 22 | to match addresseses allocated within the network you use for inter-node communication without matching any other bound addresses. 23 | 24 | For more information on creating overlay networks, see https://docs.docker.com/engine/swarm/networking/#create-an-overlay-network-in-a-swarm 25 | -------------------------------------------------------------------------------- /examples/swarm/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | seed: 5 | image: colinmollenhour/mariadb-galera-swarm 6 | environment: 7 | - XTRABACKUP_PASSWORD_FILE=/run/secrets/xtrabackup_password 8 | - MYSQL_USER=user 9 | - MYSQL_PASSWORD_FILE=/run/secrets/mysql_password 10 | - MYSQL_DATABASE=database 11 | - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password 12 | - NODE_ADDRESS=^10.0.*.* 13 | networks: 14 | - galera_network 15 | command: seed 16 | volumes: 17 | - mysql-data:/var/lib/mysql 18 | secrets: 19 | - xtrabackup_password 20 | - mysql_password 21 | - mysql_root_password 22 | node: 23 | image: colinmollenhour/mariadb-galera-swarm 24 | environment: 25 | - XTRABACKUP_PASSWORD_FILE=/run/secrets/xtrabackup_password 26 | - NODE_ADDRESS=^10.0.*.* 27 | - HEALTHY_WHILE_BOOTING=1 28 | networks: 29 | - galera_network 30 | command: node tasks.seed,tasks.node 31 | volumes: 32 | - mysql-data:/var/lib/mysql 33 | deploy: 34 | replicas: 0 35 | secrets: 36 | - xtrabackup_password 37 | 38 | volumes: 39 | mysql-data: 40 | name: '{{.Service.Name}}-{{.Task.Slot}}-data' 41 | driver: local 42 | 43 | networks: 44 | galera_network: 45 | driver: overlay 46 | 47 | secrets: 48 | xtrabackup_password: 49 | file: .secrets/xtrabackup_password 50 | mysql_password: 51 | file: .secrets/mysql_password 52 | mysql_root_password: 53 | file: .secrets/mysql_root_password 54 | -------------------------------------------------------------------------------- /healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if test -f /var/lib/mysql/pre-boot.flag; then 4 | echo "Pre-boot phase (probably waiting for GCOMM_MINIMUM)..." 5 | elif test -f /var/lib/mysql/auto-recovery.flag; then 6 | echo "Attempting auto-recovery..." 7 | elif test -f /var/lib/mysql/sst_in_progress; then 8 | echo "State Snapshot Transfer in progress..." 9 | else 10 | curl -sSf -o - localhost:8081 2>&1 || exit 1 11 | fi 12 | exit 0 13 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | DATE=$(shell date +%Y-%m-%d) 2 | VERSION_10_5=10.5.27 3 | VERSION_10_11=10.11.10 4 | VERSION_11_4=11.4.5 5 | MAINTAINER=colinmollenhour 6 | 7 | all: build 8 | 9 | build: build-10.5 build-10.11 build-11.4 10 | push: push-10.5 push-10.11 push-11.4 11 | 12 | 10.5: build-10.5 push-10.5 13 | 10.11: build-10.11 push-10.11 14 | 11.4: build-11.4 push-11.4 15 | 16 | 17 | build-10.5: 18 | docker build --pull -f Dockerfile-10.5 . -t $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_10_5)-$(DATE) 19 | test-10.5: 20 | ./test.sh $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_10_5)-$(DATE) 21 | push-10.5: 22 | docker push $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_10_5)-$(DATE) 23 | 24 | build-10.11: 25 | docker build --pull -f Dockerfile-10.11 . -t $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_10_11)-$(DATE) 26 | test-10.11: 27 | ./test.sh $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_10_11)-$(DATE) 28 | push-10.11: 29 | docker push $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_10_11)-$(DATE) 30 | 31 | build-11.4: 32 | docker build --pull -f Dockerfile-11.4 . -t $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_11_4)-$(DATE) 33 | test-11.4: 34 | ./test.sh $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_11_4)-$(DATE) 35 | push-11.4: 36 | docker push $(MAINTAINER)/mariadb-galera-swarm:$(VERSION_11_4)-$(DATE) 37 | -------------------------------------------------------------------------------- /mysqld.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script tries to start mariadbd with the right parameters to join an existing cluster 4 | # or create a new one if the old one cannot be joined 5 | # 6 | 7 | LOG_MESSAGE="===|mariadbd.sh|===:" 8 | OPT="$@" 9 | HEAD_START=15 10 | 11 | function do_install_db { 12 | if ! test -d /var/lib/mysql/mysql; then 13 | echo "${LOG_MESSAGE} Initializing MariaDb data directory..." 14 | if ! mariadb-install-db --rpm; then 15 | echo "${LOG_MESSAGE} Failed to initialize data directory." 16 | exit 1 17 | fi 18 | 19 | # Start temporary server with no networking for loading tzinfo 20 | if [[ -n $SKIP_TZINFO ]]; then return 0; fi 21 | echo "${LOG_MESSAGE} Loading timezone info..." 22 | mariadbd --skip-networking --skip-grant-tables --socket=/tmp/mysql.sock & 23 | local pid=$! 24 | sleep 3 25 | mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql --protocol=socket -uroot -hlocalhost --socket=/tmp/mysql.sock mysql 26 | if ! kill -s TERM $pid || ! wait $pid; then 27 | echo "${LOG_MESSAGE} Loading tzinfo failed." 28 | exit 1 29 | fi 30 | fi 31 | return 0 32 | } 33 | 34 | function check_nodes { 35 | for node in ${1//,/ }; do 36 | [ "$node" = "$2" ] && continue 37 | if curl -f -s -o - http://$node:8081 && echo; then 38 | echo "${LOG_MESSAGE} Node at $node is healthy!" 39 | return 0 40 | fi 41 | done 42 | return 1 43 | } 44 | 45 | function prepare_bootstrap { 46 | [[ "$OPT" =~ --wsrep-new-cluster ]] || START="--wsrep-new-cluster" 47 | if [[ -n $POSITION ]]; then 48 | START="--wsrep_start_position=$POSITION $START" 49 | fi 50 | OPT=$(<<<$OPT sed 's#\(--wsrep_cluster_address=gcomm://\)[0-9,.]*#\1#') 51 | if [[ -f /var/lib/mysql/grastate.dat ]]; then 52 | sed -i -e 's/^safe_to_bootstrap: *0/safe_to_bootstrap: 1/' /var/lib/mysql/grastate.dat 53 | fi 54 | } 55 | 56 | function fatal_error { 57 | echo "${LOG_MESSAGE} Refusing to start since something is seriously wrong.." 58 | echo "${LOG_MESSAGE} Touch /var/lib/mysql/new-cluster to force a node to start a new cluster." 59 | echo "${LOG_MESSAGE} " 60 | echo "${LOG_MESSAGE} VvVvVv " 61 | echo "${LOG_MESSAGE} |- -| // " 62 | echo "${LOG_MESSAGE} <----|O O|---<<< " 63 | echo "${LOG_MESSAGE} | D | \\\\ " 64 | echo "${LOG_MESSAGE} | () | " 65 | echo "${LOG_MESSAGE} \\__/ " 66 | echo "${LOG_MESSAGE} " 67 | rm -f /var/lib/mysql/auto-recovery.flag 68 | exit 1 69 | } 70 | 71 | # Set 'TRACE=y' environment variable to see detailed output for debugging 72 | if [ "$TRACE" = "y" ]; then 73 | set -x 74 | fi 75 | 76 | if [[ "$OPT" =~ --wsrep-new-cluster ]] 77 | then 78 | # --wsrep-new-cluster is used for the "seed" command so no recovery used 79 | echo "${LOG_MESSAGE} Starting a new cluster..." 80 | do_install_db 81 | prepare_bootstrap 82 | 83 | elif ! test -f /var/lib/mysql/ibdata1 84 | then 85 | # Skip recovery on empty data directory 86 | echo "${LOG_MESSAGE} No ibdata1 found, starting a fresh node..." 87 | do_install_db 88 | 89 | else 90 | # Try to recover state from grastate.dat or logfile 91 | if [[ $HEALTHY_WHILE_BOOTING -eq 1 ]]; then 92 | touch /var/lib/mysql/auto-recovery.flag 93 | fi 94 | POSITION='' 95 | SAFE_TO_BOOTSTRAP=-1 96 | if ! test -f /var/lib/mysql/grastate.dat; then 97 | echo "${LOG_MESSAGE} Missing grastate.dat file..." 98 | elif ! grep -q 'seqno:' /var/lib/mysql/grastate.dat; then 99 | echo "${LOG_MESSAGE} Invalid grastate.dat file..." 100 | elif grep -q '00000000-0000-0000-0000-000000000000' /var/lib/mysql/grastate.dat; then 101 | echo "${LOG_MESSAGE} uuid is not known..." 102 | else 103 | uuid=$(awk '/^uuid:/{print $2}' /var/lib/mysql/grastate.dat) 104 | seqno=$(awk '/^seqno:/{print $2}' /var/lib/mysql/grastate.dat) 105 | SAFE_TO_BOOTSTRAP=$(awk '/^safe_to_bootstrap:/{print $2}' /var/lib/mysql/grastate.dat) 106 | if [ "$seqno" = "-1" ]; then 107 | echo "${LOG_MESSAGE} uuid is known but seqno is not..." 108 | elif [ -n "$uuid" ] && [ -n "$seqno" ]; then 109 | POSITION="$uuid:$seqno" 110 | echo "${LOG_MESSAGE} Recovered position from grastate.dat: $POSITION" 111 | echo "${LOG_MESSAGE} Safe to bootstrap: $SAFE_TO_BOOTSTRAP" 112 | else 113 | echo "${LOG_MESSAGE} The grastate.dat file appears to be corrupt:" 114 | echo "##########################" 115 | cat /var/lib/mysql/grastate.dat 116 | echo "##########################" 117 | fi 118 | fi 119 | 120 | if [[ -z $POSITION ]]; then 121 | echo "${LOG_MESSAGE} --------------------------------------------------" 122 | echo "${LOG_MESSAGE} Attempting to recover GTID positon..." 123 | 124 | tmpfile=$(mktemp -t wsrep_recover.XXXXXX) 125 | mariadbd --wsrep-on=ON \ 126 | --wsrep_sst_method=skip \ 127 | --wsrep_cluster_address=gcomm:// \ 128 | --skip-networking \ 129 | --wsrep-recover 2> $tmpfile 130 | if [ $? -ne 0 ]; then cat $tmpfile; else grep 'WSREP' $tmpfile; fi 131 | echo "${LOG_MESSAGE} --------------------------------------------------" 132 | 133 | POSITION=$(sed -n 's/.*WSREP: Recovered position:\s*//p' $tmpfile) 134 | rm -f $tmpfile 135 | fi 136 | 137 | NODE_ADDRESS=$(<<<$OPT sed -E 's#.*--wsrep_node_address=([0-9\.:]+).*#\1#') 138 | GCOMM=$(<<<$OPT sed -E 's#.*gcomm://([0-9\.,]+)\s+.*#\1#') 139 | 140 | if [[ -z $POSITION ]] 141 | then 142 | # If unable to find position then something is really wrong and cluster is possibly corrupt 143 | echo "${LOG_MESSAGE} We found no wsrep position!" 144 | fatal_error 145 | 146 | elif check_nodes $GCOMM $NODE_ADDRESS 147 | then 148 | # Use the galera-healthcheck server to determine if a healthy node exists 149 | echo "${LOG_MESSAGE} Found a healthy node! Attempting to join..." 150 | START="--wsrep_start_position=$POSITION" 151 | 152 | else 153 | # Communicate to other nodes to find if there is a Primary Component and if not 154 | # figure out who has the highest recovery position to be the bootstrapper 155 | LISTEN_PORT=3309 156 | EXPECT_NODES=3 # Ideally we have a three-node cluster. This will be adjusted down to 2 later 157 | 158 | if [[ -f /var/lib/mysql/gvwstate.dat ]] 159 | then 160 | # gvwstate.dat is only useful if all nodes have the same view so we will check 161 | VIEW_ID=$( $tmpfile 179 | if [[ -n $VIEW_ID ]]; then 180 | echo "view:$NODE_ADDRESS:$VIEW_ID" >> $tmpfile 181 | fi 182 | socat -u TCP-LISTEN:$LISTEN_PORT,bind=$NODE_ADDRESS,fork OPEN:$tmpfile,append & 183 | PID_SERVER=$! 184 | 185 | # Send state data to other nodes - every 5 seconds for 3 minutes or until all nodes reached 186 | SENT_NODES='' 187 | for i in {36..0}; do 188 | # Allow user to touch flag file during startup 189 | if [[ -f /var/lib/mysql/new-cluster ]]; then 190 | echo "Found 'new-cluster' flag file. Starting new cluster." 191 | rm -f /var/lib/mysql/new-cluster 192 | prepare_bootstrap 193 | break 194 | fi 195 | for node in ${GCOMM//,/ }; do 196 | [[ $node = $NODE_ADDRESS ]] && continue 197 | sort -u $tmpfile | socat - TCP:$node:$LISTEN_PORT > /dev/null 198 | if [ "$?" = 0 ]; then 199 | SENT_NODES="$SENT_NODES,$node" 200 | fi 201 | done 202 | if [[ $(<$tmpfile grep -vF :$NODE_ADDRESS: | awk -F: '/^seqno:/{print $2}' | sort -u | wc -w) -ge $EXPECT_NODES ]] \ 203 | && [[ $(<<<$SENT_NODES tr ',' '\n' | sort -u | wc -w) -ge $EXPECT_NODES ]] 204 | then 205 | echo "${LOG_MESSAGE} Completed communication with $EXPECT_NODES other nodes." 206 | # Wait for any nodes that may still want to send to me 207 | sleep 11 208 | break 209 | fi 210 | 211 | # Check for a node coming up while we're waiting 212 | if check_nodes $GCOMM $NODE_ADDRESS; then 213 | echo "${LOG_MESSAGE} Found a healthy node, attempting to join..." 214 | START="--wsrep_start_position=$POSITION" 215 | [[ -n $VIEW_ID ]] && rm -f /var/lib/mysql/gvwstate.dat 216 | break 217 | fi 218 | 219 | # Merge in any nodes we have received data from so that we will also send data to them 220 | if [[ -s $tmpfile ]]; then 221 | _GCOMM="$GCOMM,$(<$tmpfile grep -vF :$NODE_ADDRESS: | awk -F: '/^seqno:/{print $2}' | sort -u | paste -sd ',')" 222 | GCOMM=$(<<<"${_GCOMM%%,}" sed 's/,\+/,/g' | tr ',' '\n' | sort -u | paste -sd ',') 223 | OPT=$(<<<"$OPT" sed -E "s#gcomm://[0-9\\.,]+#gcomm://$GCOMM#") 224 | fi 225 | 226 | if [[ $i -eq 24 ]]; then 227 | echo "${LOG_MESSAGE} Could not communicate with at least $EXPECT_NODES other nodes and no nodes are up..." 228 | if [[ $EXPECT_NODES -gt 1 ]]; then 229 | EXPECT_NODES=$((EXPECT_NODES - 1)) 230 | echo "${LOG_MESSAGE} Reducing expected nodes to $EXPECT_NODES after having waited for one minute..." 231 | fi 232 | elif [[ $i -eq 0 ]]; then 233 | echo "${LOG_MESSAGE} Could not communicate with at least $EXPECT_NODES other nodes and no nodes are up... Giving up!" 234 | fatal_error 235 | fi 236 | sleep 5 237 | done 238 | kill $PID_SERVER 239 | set +m 240 | 241 | # We now have a collection of lines for all *other* running nodes with lines like: 242 | # seqno:::: 243 | # view:: 244 | 245 | if [[ -n $START ]] 246 | then 247 | # Do nothing, we already know what to do 248 | true 249 | 250 | elif ! [[ -s $tmpfile ]] 251 | then 252 | # Did not receive communication from other nodes 253 | echo "${LOG_MESSAGE} No communication received from other nodes." 254 | fatal_error 255 | 256 | elif [[ -n $VIEW_ID ]] 257 | then 258 | # If all nodes have consistent views then we will maybe use gvwstate.dat to restore previous state 259 | NUM_VIEWS=$(<$tmpfile awk -F: "BEGIN{print \"$VIEW_ID\"} /^view:/{print \$3}" | sort -u | wc -l) 260 | if [ $NUM_VIEWS -eq 1 ] 261 | then 262 | LOCAL_MEMBERS=$(grep '^member:' /var/lib/mysql/gvwstate.dat | wc -l) 263 | TOTAL_MEMBERS=$(grep '^view:' $tmpfile | sort -u | wc -l) 264 | echo "${LOG_MESSAGE} Cluster has consistent view, checking presence of all $LOCAL_MEMBERS members..." 265 | if [[ $LOCAL_MEMBERS -eq $TOTAL_MEMBERS ]]; then 266 | # Entire cluster was shut down and restarted at once, will restore old Primary Component 267 | echo "${LOG_MESSAGE} gvwstate.dat file appears valid on all nodes" 268 | TOTAL_SEQNOS=$(<$tmpfile awk -F: "BEGIN{print \"$POSITION\"} /^seqno:/{print \$3 \":\" \$4}" | sort -u | wc -l) 269 | if [[ $TOTAL_SEQNOS -eq 1 ]]; then 270 | echo "${LOG_MESSAGE} All nodes have same seqno so using gvwstate.dat" 271 | START="--wsrep_start_position=$POSITION" 272 | else 273 | echo "${LOG_MESSAGE} Will not use gvwstate because mis-matching seqnos would cause SST" 274 | rm /var/lib/mysql/gvwstate.dat 275 | fi 276 | else 277 | # Not all members are present so PC cannot be restored 278 | echo "${LOG_MESSAGE} Not all members have gvwstate.dat file or are present" 279 | rm /var/lib/mysql/gvwstate.dat 280 | fi 281 | else 282 | echo "${LOG_MESSAGE} Cluster has more than one view, deleting gvwstate.dat" 283 | rm /var/lib/mysql/gvwstate.dat 284 | fi 285 | fi 286 | 287 | if [[ -z $START ]] 288 | then 289 | # Prefer to choose node using safe_to_bootstrap flag 290 | SAFE_NODES=($(<$tmpfile awk -F: '/^seqno:/{ if ($5=="1") print $2}' | sort -u)) 291 | case ${#SAFE_NODES[@]} in 292 | 0) 293 | echo "${LOG_MESSAGE} No nodes are safe_to_bootstrap. Falling back to uuid/seqno method." 294 | ;; 295 | 1) 296 | if [[ ${SAFE_NODES[0]} = $NODE_ADDRESS ]]; then 297 | echo "${LOG_MESSAGE} This node is safe_to_bootstrap! Starting a new cluster..." 298 | prepare_bootstrap 299 | else 300 | echo "${LOG_MESSAGE} Another node is safe_to_bootstrap. Will attempt to join shortly..." 301 | START="--wsrep_start_position=$POSITION" 302 | sleep $HEAD_START 303 | fi 304 | ;; 305 | *) 306 | echo "${LOG_MESSAGE} Multiple nodes are safe_to_bootstrap. Falling back to uuid/seqno method." 307 | ;; 308 | esac 309 | fi 310 | 311 | if [[ -z $START ]] 312 | then 313 | # Otherwise we will start a new Primary Component with the best node by position 314 | MY_SEQNO=${POSITION#*:} 315 | BEST_SEQNO=$(<$tmpfile grep -v :$NODE_ADDRESS: | awk -F: '/^seqno:/{print $4}' | sort -nu | tail -n 1) 316 | echo "${LOG_MESSAGE} Comparing my seqno ($MY_SEQNO) to the best other node seqno ($BEST_SEQNO)..." 317 | if [ "$MY_SEQNO" -gt "$BEST_SEQNO" ]; then 318 | # This node is newer than all the others, start a new cluster 319 | echo "${LOG_MESSAGE} This node is newer than all the others. Starting a new cluster..." 320 | prepare_bootstrap 321 | elif [ "$MY_SEQNO" -lt "$BEST_SEQNO" ]; then 322 | # This node is older than another node, be a joiner 323 | echo "${LOG_MESSAGE} This node is older than another node. Will be a joiner..." 324 | START="--wsrep_start_position=$POSITION" 325 | sleep $HEAD_START 326 | else 327 | # This and another node or nodes are the newest, lowest IP wins 328 | LOWEST_IP=$(<$tmpfile awk -F: "/^seqno:/{ if (\$4==\"$BEST_SEQNO\") print \$2 }" | sort -u | head -n 1) 329 | if [ "$NODE_ADDRESS" = "$LOWEST_IP" ]; then 330 | echo "${LOG_MESSAGE} This node is equal to the most advanced and has the lowest IP. Starting a new cluster..." 331 | prepare_bootstrap 332 | else 333 | echo "${LOG_MESSAGE} This node is the most advanced but another node ($LOWEST_IP) has a lower IP. Will be a joiner..." 334 | START="--wsrep_start_position=$POSITION" 335 | sleep $HEAD_START 336 | fi 337 | fi 338 | fi 339 | fi 340 | rm -f /var/lib/mysql/auto-recovery.flag 341 | fi 342 | 343 | # Support "truly healthy" healthchecks by listening on a new port (forwarded to the main healthcheck) 344 | # We support TCP and HTTP healthchecks by not listening until the main healthcheck reports healthy status 345 | if [[ $LISTEN_WHEN_HEALTHY -gt 0 ]]; then 346 | while true; do 347 | if curl -sSf -o /dev/null localhost:8080 2>/dev/null; then 348 | socat TCP4-LISTEN:$LISTEN_WHEN_HEALTHY,fork TCP4:localhost:8080 & 349 | break 350 | fi 351 | sleep 10 352 | done & 353 | fi 354 | 355 | # Start mariadbd 356 | echo "${LOG_MESSAGE} ---------------------------------------------------------------" 357 | echo "${LOG_MESSAGE} Starting with options: $OPT $START" 358 | exec mariadbd $OPT $START 359 | 360 | -------------------------------------------------------------------------------- /no-galera-healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | socat \ 4 | TCP-LISTEN:$1,crlf,reuseaddr,fork \ 5 | SYSTEM:" 6 | echo 'HTTP/1.0 200 OK'; 7 | echo 'Content-Type: text/plain'; 8 | echo 'Connection: close'; 9 | echo; 10 | echo 'Fake report: healthy'; 11 | " 12 | -------------------------------------------------------------------------------- /notify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "CLUSTER STATUS CHANGED: $@" >> /tmp/mysql-console/fifo 3 | -------------------------------------------------------------------------------- /primary-component.sql: -------------------------------------------------------------------------------- 1 | -- Execute this command to force a node to form a new cluster online 2 | -- Useful in case a majority of nodes are down and a quorum cannot be formed 3 | -- and you don't remember the exact variable name or value needed. :) 4 | -- 5 | -- See https://www.percona.com/blog/2014/09/01/galera-replication-how-to-recover-a-pxc-cluster/ 6 | -- 7 | SET GLOBAL wsrep_provider_options='pc.bootstrap=true'; 8 | -------------------------------------------------------------------------------- /run-upgrades.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while true; do 4 | if [[ ! -f /var/lib/mysql/sst_in_progress ]] && curl -sf -o /dev/null localhost:8080; then 5 | break 6 | fi 7 | echo "$0: waiting for server to become available..." 8 | sleep 10 9 | done 10 | 11 | version=$(mariadb -sNe "SELECT VERSION();") 12 | if [[ -z $version ]]; then 13 | echo "$0: _-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^-^_" 14 | echo "$0: = Could not login as root to determine MySQL version and run upgrades! =" 15 | echo "$0: ------------------------------------------------------------------------" 16 | exit 1 17 | fi 18 | 19 | FLAG=/var/lib/mysql/run-upgrades.flag 20 | old_version= 21 | if [[ -f $FLAG ]]; then 22 | old_version=$(grep -v '#' $FLAG) 23 | fi 24 | 25 | if [[ -z $old_version ]]; then 26 | echo -e "# Created by $0 on $(date)\n# DO NOT DELETE THIS FILE\n$version" > $FLAG 27 | 28 | # Special case for 10.1 users upgrading to 10.2 29 | if ! mariadb -sNe "SHOW GRANTS FOR 'xtrabackup'@'localhost';" | grep -qF PROCESS; then 30 | echo "$0: Granting PROCESS to xtrabackup user for old version." 31 | mariadb -e "GRANT PROCESS ON *.* TO 'xtrabackup'@'localhost'; FLUSH PRIVILEGES;" 32 | fi 33 | fi 34 | 35 | if [[ -n $old_version ]] && [[ $version != $old_version ]]; then 36 | echo -e "# Created by $0 on $(date)\n# DO NOT DELETE THIS FILE\n$version" > $FLAG 37 | echo "$0: Detected old version ($old_version -> $version)" 38 | echo "$0: Running mariadb-upgrade..." 39 | mariadb-upgrade 40 | fi 41 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on errors but report line number 4 | set -e 5 | function err_report () { 6 | echo "start.sh: Trapped error on line $1" 7 | exit 8 | } 9 | trap 'err_report $LINENO' ERR 10 | 11 | function shutdown () { 12 | echo "Received TERM|INT signal." 13 | if [[ -f /var/run/mysqld/mysqld.pid ]] && [[ -n $SYSTEM_PASSWORD ]]; then 14 | echo "Shutting down..." 15 | mariadb -u system -h 127.0.0.1 -p$SYSTEM_PASSWORD -e 'SHUTDOWN' 16 | # Since this is docker, expect that if we don't shut down quickly enough we will get killed anyway 17 | else 18 | exit 19 | fi 20 | } 21 | trap shutdown TERM INT 22 | 23 | # Set 'TRACE=y' environment variable to see detailed output for debugging 24 | if [ "$TRACE" = "y" ]; then 25 | set -x 26 | fi 27 | 28 | # Set MariaDB's time zone 29 | if [[ -n $SKIP_TZINFO ]]; then 30 | # We're skipping timezone tables population, so we restrict the 31 | # timezone format to numeric. 32 | DEFAULT_TIME_ZONE=${DEFAULT_TIME_ZONE:-"+00:00"} 33 | 34 | if [[ $DEFAULT_TIME_ZONE != *":"* ]]; then 35 | echo "Timezone '$DEFAULT_TIME_ZONE' cannot be used, because 'SKIP_TZINFO' is set. Falling back to default." 36 | DEFAULT_TIME_ZONE="+00:00" 37 | fi 38 | else 39 | # If we're populating timezone tables, we are able to use both verbal and 40 | # numeric timezone formats: "CET" or "+01:00". The first format is commonly 41 | # used with the `TZ` envvar, which can be overriden by a more specific 42 | # `DEFAULT_TIME_ZONE`. 43 | # 44 | # The default value is "+00:00". 45 | # 46 | if [[ -z $DEFAULT_TIME_ZONE ]]; then 47 | if [[ -n $TZ ]]; then 48 | DEFAULT_TIME_ZONE=$TZ 49 | else 50 | DEFAULT_TIME_ZONE="+00:00" 51 | fi 52 | fi 53 | fi 54 | 55 | # Set data directory permissions for later use of "gosu" 56 | chown mysql /var/lib/mysql 57 | 58 | # 59 | # Utility modes 60 | # 61 | case "$1" in 62 | sleep) 63 | echo "Sleeping forever..." 64 | trap - TERM INT 65 | sleep infinity 66 | exit 67 | ;; 68 | no-galera) 69 | echo "Starting with Galera disabled" 70 | shift 1 71 | 72 | # Allow for easily adding more startup scripts 73 | if [ -f /usr/local/lib/startup.sh ]; then 74 | source /usr/local/lib/startup.sh "$@" 75 | fi 76 | 77 | # Allow for scripts above to create a one-time use init-file 78 | if [ -f /var/lib/mysql/init-file.sql ]; then 79 | mv /var/lib/mysql/init-file.sql /tmp/init-file.sql 80 | set -- "$@" --init-file=/tmp/init-file.sql 81 | fi 82 | 83 | set +e -m 84 | gosu mysql mariadbd --console \ 85 | --wsrep-on=OFF \ 86 | --default-time-zone=$DEFAULT_TIME_ZONE \ 87 | "$@" 2>&1 & 88 | mysql_pid=$! 89 | 90 | # Start fake healthcheck 91 | if [[ -n $FAKE_HEALTHCHECK ]]; then 92 | no-galera-healthcheck.sh $FAKE_HEALTHCHECK >/dev/null & 93 | fi 94 | 95 | wait $mysql_pid || true 96 | exit 97 | ;; 98 | bash) 99 | shift 1 100 | trap - TERM INT 101 | exec /bin/bash "$@" 102 | ;; 103 | seed|node) 104 | START_MODE=$1 105 | shift 106 | ;; 107 | *) 108 | echo "sleep|no-galera|bash|seed|node ,..." 109 | exit 1 110 | esac 111 | 112 | # 113 | # Resolve node address 114 | # 115 | if [ -z "$NODE_ADDRESS" ]; then 116 | # Support Weave/Kontena 117 | NODE_ADDRESS=$(ip addr | awk '/inet/ && /ethwe/{sub(/\/.*$/,"",$2); print $2}') 118 | fi 119 | if [ -z "$NODE_ADDRESS" ]; then 120 | # Support Docker Swarm Mode 121 | NODE_ADDRESS=$(ip addr | awk '/inet/ && /eth0/{sub(/\/.*$/,"",$2); print $2}' | head -n 1) 122 | elif [[ "$NODE_ADDRESS" =~ [a-zA-Z][a-zA-Z0-9:]+ ]]; then 123 | # Support interface - e.g. Docker Swarm Mode uses eth0 124 | NODE_ADDRESS=$(ip addr | awk "/inet/ && / $NODE_ADDRESS\$/{sub(/\\/.*$/,\"\",\$2); print \$2}" | head -n 1) 125 | elif ! [[ "$NODE_ADDRESS" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 126 | # Support grep pattern. E.g. ^10.0.1.* 127 | NODE_ADDRESS=$(getent hosts $(hostname) | awk '{print $1}' | grep -e "$NODE_ADDRESS") 128 | fi 129 | if ! [[ "$NODE_ADDRESS" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 130 | echo "Could not determine NODE_ADDRESS: $NODE_ADDRESS" 131 | exit 1 132 | fi 133 | echo "...------======------... MariaDB Galera Start Script ...------======------..." 134 | echo "Got NODE_ADDRESS=$NODE_ADDRESS" 135 | 136 | MYSQL_MODE_ARGS="" 137 | 138 | # Allow for easily adding more startup scripts 139 | export NODE_ADDRESS 140 | if [ -f /usr/local/lib/startup.sh ]; then 141 | source /usr/local/lib/startup.sh "$@" 142 | fi 143 | 144 | # 145 | # Read optional secrets from files 146 | # 147 | 148 | # mode is xtrabackup? 149 | if [[ $SST_METHOD =~ ^(xtrabackup|mariabackup) ]] ; then 150 | XTRABACKUP_PASSWORD_FILE=${XTRABACKUP_PASSWORD_FILE:-/run/secrets/xtrabackup_password} 151 | if [ -z $XTRABACKUP_PASSWORD ] && [ -f $XTRABACKUP_PASSWORD_FILE ]; then 152 | XTRABACKUP_PASSWORD=$(cat $XTRABACKUP_PASSWORD_FILE) 153 | fi 154 | [ -z "$XTRABACKUP_PASSWORD" ] && echo "WARNING: XTRABACKUP_PASSWORD is empty" 155 | MYSQL_MODE_ARGS+=" --wsrep_sst_auth=xtrabackup:$XTRABACKUP_PASSWORD" 156 | fi 157 | 158 | SYSTEM_PASSWORD_FILE=${SYSTEM_PASSWORD_FILE:-/run/secrets/system_password} 159 | if [ -z $SYSTEM_PASSWORD ] && [ -f $SYSTEM_PASSWORD_FILE ]; then 160 | SYSTEM_PASSWORD=$(cat $SYSTEM_PASSWORD_FILE) 161 | fi 162 | if [ -z "$SYSTEM_PASSWORD" ]; then 163 | if [ -n "$XTRABACKUP_PASSWORD" ]; then 164 | SYSTEM_PASSWORD=$(echo "$XTRABACKUP_PASSWORD" | sha256sum | awk '{print $1;}') 165 | else 166 | echo "SYSTEM_PASSWORD not set" 167 | exit 1 168 | fi 169 | fi 170 | 171 | CLUSTER_NAME=${CLUSTER_NAME:-cluster} 172 | GCOMM_MINIMUM=${GCOMM_MINIMUM:-2} 173 | GCOMM="" 174 | 175 | # Hold startup until the flag file is deleted 176 | if [[ -f /var/lib/mysql/hold-start ]]; then 177 | echo "Waiting for 'hold-start' flag file to be deleted..." 178 | while [[ -f /var/lib/mysql/hold-start ]]; do 179 | sleep 10 180 | done 181 | fi 182 | 183 | # Allow "node" to be "seed" if "new-cluster" file is present 184 | # In this case the MYSQL_ROOT_PASSWORD may be specified within the file 185 | if [[ $START_MODE = "node" ]] && [[ -f /var/lib/mysql/new-cluster ]]; then 186 | START_MODE=seed 187 | shift # get rid of node argument 188 | if [[ -s /var/lib/mysql/new-cluster ]]; then 189 | MYSQL_ROOT_PASSWORD="$(cat /var/lib/mysql/new-cluster)" 190 | fi 191 | rm -f /var/lib/mysql/new-cluster 192 | fi 193 | 194 | # Generate init file to create required users 195 | if ( [ "$START_MODE" = "node" ] && [ -f /var/lib/mysql/force-cluster-bootstrapping ] ) \ 196 | || ( [ "$START_MODE" = "seed" ] && ! [ -f /var/lib/mysql/skip-cluster-bootstrapping ] ) 197 | then 198 | echo "Generating cluster bootstrap script..." 199 | MYSQL_ROOT_PASSWORD_FILE=${MYSQL_ROOT_PASSWORD_FILE:-/run/secrets/mysql_root_password} 200 | MYSQL_ROOT_HOST_FILE=${MYSQL_ROOT_HOST_FILE:-/run/secrets/mysql_root_host} 201 | MYSQL_PASSWORD_FILE=${MYSQL_PASSWORD_FILE:-/run/secrets/mysql_password} 202 | MYSQL_DATABASE_FILE=${MYSQL_DATABASE_FILE:-/run/secrets/mysql_database} 203 | if [ -z $MYSQL_ROOT_PASSWORD ] && [ -f $MYSQL_ROOT_PASSWORD_FILE ]; then 204 | MYSQL_ROOT_PASSWORD=$(cat $MYSQL_ROOT_PASSWORD_FILE) 205 | fi 206 | if [ -z $MYSQL_ROOT_HOST ] && [ -f $MYSQL_ROOT_HOST_FILE ]; then 207 | MYSQL_ROOT_HOST=$(cat $MYSQL_ROOT_HOST_FILE) 208 | fi 209 | if [ -z $MYSQL_PASSWORD ] && [ -f $MYSQL_PASSWORD_FILE ]; then 210 | MYSQL_PASSWORD=$(cat $MYSQL_PASSWORD_FILE) 211 | fi 212 | if [ -z $MYSQL_DATABASE ] && [ -f $MYSQL_DATABASE_FILE ]; then 213 | MYSQL_DATABASE=$(cat $MYSQL_DATABASE_FILE) 214 | fi 215 | if [ -z "$MYSQL_ROOT_PASSWORD" ]; then 216 | MYSQL_ROOT_PASSWORD=$(head -c 32 /dev/urandom | base64 | head -c 32) 217 | echo "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD" 218 | fi 219 | if [ -z "$MYSQL_ROOT_HOST" ]; then 220 | MYSQL_ROOT_HOST='127.0.0.1' 221 | fi 222 | 223 | >/tmp/bootstrap.sql 224 | 225 | # Create 'root' user 226 | cat >> /tmp/bootstrap.sql <> /tmp/bootstrap.sql <> /tmp/bootstrap.sql <> /tmp/bootstrap.sql <>/tmp/bootstrap.sql <>/tmp/bootstrap.sql <> /tmp/bootstrap.sql 263 | fi 264 | 265 | if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then 266 | echo "CREATE USER IF NOT EXISTS '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" >> /tmp/bootstrap.sql 267 | if [ "$MYSQL_DATABASE" ]; then 268 | echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" >> /tmp/bootstrap.sql 269 | fi 270 | fi 271 | echo "FLUSH PRIVILEGES;" >> /tmp/bootstrap.sql 272 | 273 | # Add additional database initialization scripts 274 | for f in /docker-entrypoint-initdb.d/*; do 275 | case "$f" in 276 | *.sh) echo "$0: running $f"; . "$f" ;; 277 | *.sql) echo "$0: appending $f"; cat "$f" >> /tmp/bootstrap.sql ;; 278 | *.sql.gz) echo "$0: appending $f"; gunzip -c "$f" >> /tmp/bootstrap.sql ;; 279 | *) echo "$0: ignoring $f" ;; 280 | esac 281 | echo 282 | done 283 | 284 | MYSQL_MODE_ARGS+=" --init-file=/tmp/bootstrap.sql" 285 | rm -f /var/lib/mysql/force-cluster-bootstrapping 286 | touch /var/lib/mysql/skip-cluster-bootstrapping 287 | fi 288 | 289 | # 290 | # Start modes: 291 | # - seed - Start a new cluster - run only once and use 'node' after cluster is started 292 | # - node - Join an existing cluster 293 | # 294 | case $START_MODE in 295 | seed) 296 | MYSQL_MODE_ARGS+=" --wsrep-on=ON --wsrep-new-cluster --wsrep-sst-method=$SST_METHOD" 297 | echo "Starting seed node" 298 | ;; 299 | node) 300 | ADDRS="$1" 301 | shift 302 | if [[ -z $ADDRS ]]; then 303 | echo "List of nodes addresses/hostnames required" 304 | exit 1 305 | fi 306 | MYSQL_MODE_ARGS+=" --wsrep-on=ON --wsrep-sst-method=$SST_METHOD" 307 | RESOLVE=0 308 | SLEEPS=0 309 | 310 | # Begin service discovery of other node addresses 311 | while true; do 312 | # Allow user to touch flag file during startup 313 | if [[ -f /var/lib/mysql/new-cluster ]]; then 314 | MYSQL_MODE_ARGS+=" --wsrep-new-cluster" 315 | echo "Found 'new-cluster' flag file. Starting new cluster." 316 | rm -f /var/lib/mysql/new-cluster 317 | break 318 | fi 319 | SEP="" 320 | GCOMM="" 321 | for ADDR in ${ADDRS//,/ }; do 322 | if [[ "$ADDR" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 323 | GCOMM+="$SEP$ADDR" 324 | else 325 | RESOLVE=1 326 | GCOMM+="$SEP$(getent hosts "$ADDR" | awk '{ print $1 }' | paste -sd ",")" 327 | fi 328 | if [ -n "$GCOMM" ]; then 329 | SEP=, 330 | fi 331 | done 332 | GCOMM=$(echo "$GCOMM" | sed 's/[, ]\+/,/g') # strip duplicate commas and whitespace 333 | GCOMM=${GCOMM#,} # strip leading commas 334 | GCOMM=${GCOMM%,} # strip trailing commas 335 | 336 | echo "Found Servers: $GCOMM" 337 | 338 | # Allow user to bypass waiting for other IPs 339 | if [[ -f /var/lib/mysql/skip-gcomm-wait ]]; then 340 | break 341 | fi 342 | 343 | # It is possible that containers on other nodes aren't running yet and should be waited on 344 | # before trying to start. For example, this occurs when updated container images are being pulled 345 | # by `docker service update ` or on a full cluster power loss 346 | COUNT=$(echo "$GCOMM" | tr ',' "\n" | sort -u | grep -v -e "^$NODE_ADDRESS\$" -e '^$' | wc -l) 347 | if [ $RESOLVE -eq 1 ] && [ $COUNT -lt $(($GCOMM_MINIMUM - 1)) ]; then 348 | 349 | # Bypass healthcheck so we can keep waiting for other nodes to appear 350 | if [[ $HEALTHY_WHILE_BOOTING -eq 1 ]]; then 351 | touch /var/lib/mysql/pre-boot.flag 352 | fi 353 | 354 | echo "Waiting for at least $GCOMM_MINIMUM IP addresses to resolve..." 355 | SLEEPS=$((SLEEPS + 1)) 356 | sleep 3 357 | else 358 | break 359 | fi 360 | 361 | # After 90 seconds reduce GCOMM_MINIMUM 362 | if [ $SLEEPS -ge 30 ]; then 363 | SLEEPS=0 364 | GCOMM_MINIMUM=$((GCOMM_MINIMUM - 1)) 365 | echo "Reducing GCOMM_MINIMUM to $GCOMM_MINIMUM" 366 | fi 367 | done 368 | # Pre-boot completed 369 | rm -f /var/lib/mysql/pre-boot.flag 370 | echo "Starting node, connecting to gcomm://$GCOMM" 371 | ;; 372 | esac 373 | 374 | 375 | # start processes 376 | set +e -m 377 | 378 | # Allow external processes to write to docker logs (wsrep_notify_cmd) 379 | # Place it in a directory that is not writeable by mariadb to prevent SST script from deleting it 380 | fifo=/tmp/mysql-console/fifo 381 | rm -rf $(dirname $fifo) \ 382 | && mkdir -p $(dirname $fifo) \ 383 | && chmod 755 $(dirname $fifo) \ 384 | && mkfifo $fifo \ 385 | && chmod o+rw $fifo \ 386 | && echo "Tailing $fifo..." \ 387 | && tail -f $fifo & 388 | tail_pid=$! 389 | 390 | # Port 8080 only reports healthy when ready to serve clients 391 | # Use this one for load balancer health checks 392 | galera-healthcheck -user=system -password="$SYSTEM_PASSWORD" \ 393 | -port=8080 \ 394 | -availWhenDonor=false \ 395 | -availWhenReadOnly=false \ 396 | -pidfile=/var/run/galera-healthcheck-1.pid >/dev/null & 397 | 398 | # Port 8081 reports healthy as long as the server is synced or donor/desynced state 399 | # Use this one to help other nodes determine cluster state before launching server 400 | galera-healthcheck -user=system -password="$SYSTEM_PASSWORD" \ 401 | -port=8081 \ 402 | -availWhenDonor=true \ 403 | -availWhenReadOnly=true \ 404 | -pidfile=/var/run/galera-healthcheck-2.pid >/dev/null & 405 | 406 | # Run automated upgrades 407 | if [[ -z $SKIP_UPGRADES ]] && [[ ! -f /var/lib/mysql/skip-upgrades ]]; then 408 | sleep 5 && run-upgrades.sh || true & 409 | fi 410 | 411 | gosu mysql mysqld.sh --console \ 412 | $MYSQL_MODE_ARGS \ 413 | --wsrep_cluster_name=$CLUSTER_NAME \ 414 | --wsrep_cluster_address=gcomm://${GCOMM%,} \ 415 | --wsrep_node_address=$NODE_ADDRESS \ 416 | --default-time-zone=$DEFAULT_TIME_ZONE \ 417 | "$@" 2>&1 & 418 | 419 | wait $! || true 420 | RC=$? 421 | 422 | echo "MariaDB exited with return code ($RC)" 423 | test -f /var/lib/mysql/grastate.dat && cat /var/lib/mysql/grastate.dat 424 | 425 | test -s /var/run/galera-healthcheck-1.pid && kill $(cat /var/run/galera-healthcheck-1.pid) 426 | test -s /var/run/galera-healthcheck-2.pid && kill $(cat /var/run/galera-healthcheck-2.pid) 427 | 428 | echo "Goodbye" 429 | exit $RC 430 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | IMAGE=${1?"Usage: $0 IMAGE_NAME"} 4 | 5 | if [[ $IMAGE != "--cleanup" ]]; then 6 | docker network create cm-galera-test 7 | 8 | docker run -d --name cm-galera-test-seed --network cm-galera-test --network-alias seed \ 9 | -e XTRABACKUP_PASSWORD=foobar \ 10 | -e SKIP_TZINFO=1 \ 11 | $IMAGE seed --log-bin=mysqld-bin 12 | 13 | sleep 5 14 | docker logs cm-galera-test-seed 15 | docker run -d --name cm-galera-test-node1 --network cm-galera-test \ 16 | -e XTRABACKUP_PASSWORD=foobar \ 17 | -e SKIP_TZINFO=1 \ 18 | -e GCOMM_MINIMUM=1 \ 19 | $IMAGE node seed --log-bin=mysqld-bin 20 | sleep 5 21 | docker logs cm-galera-test-node1 22 | docker run --name cm-galera-test-node2 --network cm-galera-test \ 23 | -e XTRABACKUP_PASSWORD=foobar \ 24 | -e SKIP_TZINFO=1 \ 25 | -e GCOMM_MINIMUM=1 \ 26 | $IMAGE node seed,node1 --log-bin=mysqld-bin 27 | fi 28 | if [[ -z $SKIP_CLEANUP ]]; then 29 | echo "Cleaning up..." 30 | set +e 31 | docker stop cm-galera-test-seed 32 | docker rm -v cm-galera-test-seed 33 | docker stop cm-galera-test-node1 34 | docker rm -v cm-galera-test-node1 35 | docker stop cm-galera-test-node2 36 | docker rm -v cm-galera-test-node2 37 | docker network rm cm-galera-test 38 | fi 39 | --------------------------------------------------------------------------------