O ProxySQL oferece suporte ao clustering nativo desde a v1.4.2. Isso significa que várias instâncias do ProxySQL reconhecem clusters; eles estão cientes do estado um do outro e são capazes de lidar com as alterações de configuração automaticamente, sincronizando com a configuração mais atualizada com base na versão da configuração, carimbo de data/hora e valor da soma de verificação. Confira esta postagem de blog que demonstra como configurar o suporte de cluster para ProxySQL e como você pode esperar que ele se comporte.
ProxySQL é um proxy descentralizado, recomendado para ser implantado mais próximo da aplicação. Essa abordagem se adapta muito bem até centenas de nós, pois foi projetada para ser facilmente reconfigurável em tempo de execução. Para gerenciar com eficiência vários nós ProxySQL, é preciso garantir que as alterações realizadas em um dos nós sejam aplicadas em todos os nós do farm. Sem clustering nativo, é preciso exportar manualmente as configurações e importá-las para os outros nós (embora você possa automatizar isso sozinho).
Na postagem anterior do blog, abordamos o clustering ProxySQL por meio do Kubernetes ConfigMap. Essa abordagem é mais ou menos bastante eficiente com a abordagem de configuração centralizada no ConfigMap. O que quer que seja carregado no ConfigMap será montado em pods. A atualização da configuração pode ser feita por meio de controle de versão (modifique o conteúdo do proxysql.cnf e carregue-o no ConfigMap com outro nome) e, em seguida, envie para os pods, dependendo do agendamento do método de implantação e da estratégia de atualização.
No entanto, em um ambiente em rápida mudança, essa abordagem ConfigMap provavelmente não é o melhor método porque, para carregar a nova configuração, é necessário reprogramar o pod para remontar o volume ConfigMap e isso pode comprometer o serviço ProxySQL como um todo. Por exemplo, digamos que em nosso ambiente, nossa política de senha estrita exige forçar a expiração da senha do usuário MySQL a cada 7 dias, o que teríamos que manter atualizando o ConfigMap ProxySQL para a nova senha semanalmente. Como observação lateral, o usuário do MySQL dentro do ProxySQL requer que o usuário e a senha correspondam aos dos servidores MySQL de back-end. É aí que devemos começar a usar o suporte de cluster nativo do ProxySQL no Kubernetes, para aplicar automaticamente as alterações de configuração sem o incômodo de versionamento do ConfigMap e reprogramação de pod.
Nesta postagem do blog, mostraremos como executar o clustering nativo do ProxySQL com serviço headless no Kubernetes. Nossa arquitetura de alto nível pode ser ilustrada como abaixo:
Temos 3 nós Galera rodando em infraestrutura bare-metal implantada e gerenciada pelo ClusterControl:
- 192.168.0.21
- 192.168.0.22
- 192.168.0.23
Nossos aplicativos estão todos sendo executados como pods no Kubernetes. A ideia é introduzir duas instâncias de ProxySQL entre o aplicativo e nosso cluster de banco de dados para servir como um proxy reverso. Os aplicativos se conectarão aos pods do ProxySQL por meio do serviço Kubernetes, que terá balanceamento de carga e failover em várias réplicas do ProxySQL.
Veja a seguir um resumo de nossa configuração do Kubernetes:
[email protected]:~# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
kube1 Ready master 5m v1.15.1 192.168.100.201 <none> Ubuntu 18.04.1 LTS 4.15.0-39-generic docker://18.9.7
kube2 Ready <none> 4m1s v1.15.1 192.168.100.202 <none> Ubuntu 18.04.1 LTS 4.15.0-39-generic docker://18.9.7
kube3 Ready <none> 3m42s v1.15.1 192.168.100.203 <none> Ubuntu 18.04.1 LTS 4.15.0-39-generic docker://18.9.7
Configuração do ProxySQL via ConfigMap
Vamos primeiro preparar nossa configuração base que será carregada no ConfigMap. Crie um arquivo chamado proxysql.cnf e adicione as seguintes linhas:
datadir="/var/lib/proxysql"
admin_variables=
{
admin_credentials="proxysql-admin:adminpassw0rd;cluster1:secret1pass"
mysql_ifaces="0.0.0.0:6032"
refresh_interval=2000
cluster_username="cluster1"
cluster_password="secret1pass"
cluster_check_interval_ms=200
cluster_check_status_frequency=100
cluster_mysql_query_rules_save_to_disk=true
cluster_mysql_servers_save_to_disk=true
cluster_mysql_users_save_to_disk=true
cluster_proxysql_servers_save_to_disk=true
cluster_mysql_query_rules_diffs_before_sync=3
cluster_mysql_servers_diffs_before_sync=3
cluster_mysql_users_diffs_before_sync=3
cluster_proxysql_servers_diffs_before_sync=3
}
mysql_variables=
{
threads=4
max_connections=2048
default_query_delay=0
default_query_timeout=36000000
have_compress=true
poll_timeout=2000
interfaces="0.0.0.0:6033;/tmp/proxysql.sock"
default_schema="information_schema"
stacksize=1048576
server_version="5.1.30"
connect_timeout_server=10000
monitor_history=60000
monitor_connect_interval=200000
monitor_ping_interval=200000
ping_interval_server_msec=10000
ping_timeout_server=200
commands_stats=true
sessions_sort=true
monitor_username="proxysql"
monitor_password="proxysqlpassw0rd"
monitor_galera_healthcheck_interval=2000
monitor_galera_healthcheck_timeout=800
}
mysql_galera_hostgroups =
(
{
writer_hostgroup=10
backup_writer_hostgroup=20
reader_hostgroup=30
offline_hostgroup=9999
max_writers=1
writer_is_also_reader=1
max_transactions_behind=30
active=1
}
)
mysql_servers =
(
{ address="192.168.0.21" , port=3306 , hostgroup=10, max_connections=100 },
{ address="192.168.0.22" , port=3306 , hostgroup=10, max_connections=100 },
{ address="192.168.0.23" , port=3306 , hostgroup=10, max_connections=100 }
)
mysql_query_rules =
(
{
rule_id=100
active=1
match_pattern="^SELECT .* FOR UPDATE"
destination_hostgroup=10
apply=1
},
{
rule_id=200
active=1
match_pattern="^SELECT .*"
destination_hostgroup=20
apply=1
},
{
rule_id=300
active=1
match_pattern=".*"
destination_hostgroup=10
apply=1
}
)
mysql_users =
(
{ username = "wordpress", password = "passw0rd", default_hostgroup = 10, transaction_persistent = 0, active = 1 },
{ username = "sbtest", password = "passw0rd", default_hostgroup = 10, transaction_persistent = 0, active = 1 }
)
proxysql_servers =
(
{ hostname = "proxysql-0.proxysqlcluster", port = 6032, weight = 1 },
{ hostname = "proxysql-1.proxysqlcluster", port = 6032, weight = 1 }
)
Algumas das linhas de configuração acima são explicadas por seção abaixo:
admin_variables
Preste atenção às admin_credentials variável onde usamos usuário não padrão que é "proxysql-admin". O ProxySQL reserva o usuário "admin" padrão apenas para conexão local via localhost. Portanto, temos que usar outros usuários para acessar a instância ProxySQL remotamente. Caso contrário, você obteria o seguinte erro:
ERROR 1040 (42000): User 'admin' can only connect locally
Também anexamos o cluster_username e cluster_password valor em admin_credentials linha, separada por um ponto e vírgula para permitir a sincronização automática. Todas as variáveis prefixadas com cluster_* estão relacionados ao clustering nativo do ProxySQL e são autoexplicativos.
mysql_galera_hostgroups
Esta é uma nova diretiva introduzida para ProxySQL 2.x (nossa imagem ProxySQL está sendo executada em 2.0.5). Se você deseja executar no ProxySQL 1.x, remova esta parte e use a tabela do agendador. Já explicamos os detalhes de configuração nesta postagem do blog, Como executar e configurar o ProxySQL 2.0 para MySQL Galera Cluster no Docker em "ProxySQL 2.x Support for Galera Cluster".
mysql_servers
Todas as linhas são autoexplicativas, baseadas em três servidores de banco de dados executados no MySQL Galera Cluster, conforme resumido na captura de tela de Topologia a seguir retirada do ClusterControl:
servidores_proxysql
Aqui definimos uma lista de pares ProxySQL:
- nome do host - nome do host/endereço IP do par
- porta - porta de administração do par
- peso - não utilizado atualmente, mas no roteiro para melhorias futuras
- comentário - Campo de comentário de forma livre
No ambiente Docker/Kubernetes, existem várias maneiras de descobrir e vincular nomes de host ou endereços IP de contêiner e inseri-los nesta tabela, seja usando ConfigMap, inserção manual, via script entrypoint.sh, variáveis de ambiente ou outros meios. No Kubernetes, dependendo do método ReplicationController ou Deployment usado, adivinhar antecipadamente o nome do host resolvível do pod é um pouco complicado, a menos que você esteja executando no StatefulSet.
Confira este tutorial sobre o índice ordinal do pod StatefulState, que fornece um nome de host resolvível estável para os pods criados. Combine isso com o serviço headless (explicado mais abaixo), o formato do nome do host resolvível seria:
{app_name}-{index_number}.{service}
Onde {service} é um serviço headless, que explica de onde vêm "proxysql-0.proxysqlcluster" e "proxysql-1.proxysqlcluster". Se você quiser ter mais de 2 réplicas, adicione mais entradas de acordo, acrescentando um número de índice crescente relativo ao nome do aplicativo StatefulSet.
Agora estamos prontos para enviar o arquivo de configuração para o ConfigMap, que será montado em cada pod ProxySQL durante a implantação:
$ kubectl create configmap proxysql-configmap --from-file=proxysql.cnf
Verifique se nosso ConfigMap está carregado corretamente:
$ kubectl get configmap
NAME DATA AGE
proxysql-configmap 1 7h57m
Criando usuário de monitoramento ProxySQL
A próxima etapa antes de iniciarmos a implantação é criar um usuário de monitoramento ProxySQL em nosso cluster de banco de dados. Como estamos executando no cluster do Galera, execute as seguintes instruções em um dos nós do Galera:
mysql> CREATE USER 'proxysql'@'%' IDENTIFIED BY 'proxysqlpassw0rd';
mysql> GRANT USAGE ON *.* TO 'proxysql'@'%';
Se você não criou os usuários do MySQL (conforme especificado na seção mysql_users acima), temos que criá-los também:
mysql> CREATE USER 'wordpress'@'%' IDENTIFIED BY 'passw0rd';
mysql> GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'%';
mysql> CREATE USER 'sbtest'@'%' IDENTIFIED BY 'passw0rd';
mysql> GRANT ALL PRIVILEGES ON sbtest.* TO 'proxysql'@'%';
É isso. Agora estamos prontos para iniciar a implantação.
Implantando um StatefulSet
Começaremos criando duas instâncias do ProxySQL ou réplicas para fins de redundância usando StatefulSet.
Vamos começar criando um arquivo de texto chamado proxysql-ss-svc.yml e adicionar as seguintes linhas:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: proxysql
labels:
app: proxysql
spec:
replicas: 2
serviceName: proxysqlcluster
selector:
matchLabels:
app: proxysql
tier: frontend
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: proxysql
tier: frontend
spec:
restartPolicy: Always
containers:
- image: severalnines/proxysql:2.0.4
name: proxysql
volumeMounts:
- name: proxysql-config
mountPath: /etc/proxysql.cnf
subPath: proxysql.cnf
ports:
- containerPort: 6033
name: proxysql-mysql
- containerPort: 6032
name: proxysql-admin
volumes:
- name: proxysql-config
configMap:
name: proxysql-configmap
---
apiVersion: v1
kind: Service
metadata:
annotations:
labels:
app: proxysql
tier: frontend
name: proxysql
spec:
ports:
- name: proxysql-mysql
nodePort: 30033
port: 6033
protocol: TCP
targetPort: 6033
- name: proxysql-admin
nodePort: 30032
port: 6032
protocol: TCP
targetPort: 6032
selector:
app: proxysql
tier: frontend
type: NodePort
Existem duas seções da definição acima - StatefulSet e Service. O StatefulSet é a definição de nossos pods ou réplicas e o ponto de montagem para nosso volume ConfigMap, carregado de proxysql-configmap. A próxima seção é a definição do serviço, onde definimos como os pods devem ser expostos e roteados para a rede interna ou externa.
Crie o conjunto e o serviço com estado do ProxySQL:
$ kubectl create -f proxysql-ss-svc.yml
Verifique os estados do pod e do serviço:
$ kubectl get pods,svc
NAME READY STATUS RESTARTS AGE
pod/proxysql-0 1/1 Running 0 4m46s
pod/proxysql-1 1/1 Running 0 2m59s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10h
service/proxysql NodePort 10.111.240.193 <none> 6033:30033/TCP,6032:30032/TCP 5m28s
Se você observar o log do pod, notará que fomos inundados com este aviso:
$ kubectl logs -f proxysql-0
...
2019-08-01 19:06:18 ProxySQL_Cluster.cpp:215:ProxySQL_Cluster_Monitor_thread(): [WARNING] Cluster: unable to connect to peer proxysql-1.proxysqlcluster:6032 . Error: Unknown MySQL server host 'proxysql-1.proxysqlcluster' (0)
O acima simplesmente significa que o proxysql-0 não conseguiu resolver "proxysql-1.proxysqlcluster" e se conectar a ele, o que é esperado, pois não criamos nosso serviço headless para registros DNS que serão necessários para comunicação entre ProxySQL.
Serviço sem cabeça do Kubernetes
Para que os pods do ProxySQL possam resolver o FQDN previsto e se conectar a ele diretamente, o processo de resolução deve poder pesquisar o endereço IP do pod de destino atribuído e não o endereço IP virtual. É aqui que o serviço sem cabeça entra em cena. Ao criar um serviço headless definindo "clusterIP=None", nenhum balanceamento de carga é configurado e nenhum IP de cluster (IP virtual) é alocado para este serviço. Somente o DNS é configurado automaticamente. Ao executar uma consulta DNS para serviço sem periféricos, você obterá a lista dos endereços IP dos pods.
Aqui está o que parece se procurarmos os registros DNS do serviço headless para "proxysqlcluster" (neste exemplo, tivemos 3 instâncias do ProxySQL):
$ host proxysqlcluster
proxysqlcluster.default.svc.cluster.local has address 10.40.0.2
proxysqlcluster.default.svc.cluster.local has address 10.40.0.3
proxysqlcluster.default.svc.cluster.local has address 10.32.0.2
Enquanto a saída a seguir mostra o registro DNS para o serviço padrão chamado "proxysql" que resolve para o clusterIP:
$ host proxysql
proxysql.default.svc.cluster.local has address 10.110.38.154
Para criar um serviço headless e anexá-lo aos pods, é preciso definir o ServiceName dentro da declaração StatefulSet, e a definição do serviço deve ter "clusterIP=None" conforme mostrado abaixo. Crie um arquivo de texto chamado proxysql-headless-svc.yml e adicione as seguintes linhas:
apiVersion: v1
kind: Service
metadata:
name: proxysqlcluster
labels:
app: proxysql
spec:
clusterIP: None
ports:
- port: 6032
name: proxysql-admin
selector:
app: proxysql
Crie o serviço headless:
$ kubectl create -f proxysql-headless-svc.yml
Apenas para verificação, neste momento, temos os seguintes serviços em execução:
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 8h
proxysql NodePort 10.110.38.154 <none> 6033:30033/TCP,6032:30032/TCP 23m
proxysqlcluster ClusterIP None <none> 6032/TCP 4s
Agora, confira um dos logs do nosso pod:
$ kubectl logs -f proxysql-0
...
2019-08-01 19:06:19 ProxySQL_Cluster.cpp:215:ProxySQL_Cluster_Monitor_thread(): [WARNING] Cluster: unable to connect to peer proxysql-1.proxysqlcluster:6032 . Error: Unknown MySQL server host 'proxysql-1.proxysqlcluster' (0)
2019-08-01 19:06:19 [INFO] Cluster: detected a new checksum for mysql_query_rules from peer proxysql-1.proxysqlcluster:6032, version 1, epoch 1564686376, checksum 0x3FEC69A5C9D96848 . Not syncing yet ...
2019-08-01 19:06:19 [INFO] Cluster: checksum for mysql_query_rules from peer proxysql-1.proxysqlcluster:6032 matches with local checksum 0x3FEC69A5C9D96848 , we won't sync.
Você notaria que o componente Cluster é capaz de resolver, conectar e detectar uma nova soma de verificação do outro peer, proxysql-1.proxysqlcluster na porta 6032 por meio do serviço headless chamado "proxysqlcluster". Observe que esse serviço expõe a porta 6032 apenas na rede Kubernetes, portanto, é inacessível externamente.
Neste ponto, nossa implantação está concluída.
Conectando ao ProxySQL
Existem várias maneiras de se conectar aos serviços ProxySQL. As conexões MySQL com balanceamento de carga devem ser enviadas para a porta 6033 de dentro da rede Kubernetes e usar a porta 30033 se o cliente estiver se conectando de uma rede externa.
Para se conectar à interface de administração do ProxySQL de uma rede externa, podemos conectar à porta definida na seção NodePort, 30032 (192.168.100.203 é o endereço IP primário do host kube3.local):
$ mysql -uproxysql-admin -padminpassw0rd -h192.168.100.203 -P30032
Use o clusterIP 10.110.38.154 (definido no serviço "proxysql") na porta 6032 se quiser acessá-lo de outros pods na rede Kubernetes.
Em seguida, execute as alterações de configuração do ProxySQL conforme desejar e carregue-as no tempo de execução:
mysql> INSERT INTO mysql_users (username,password,default_hostgroup) VALUES ('newuser','passw0rd',10);
mysql> LOAD MYSQL USERS TO RUNTIME;
Você notará as seguintes linhas em um dos pods indicando que a sincronização da configuração foi concluída:
$ kubectl logs -f proxysql-0
...
2019-08-02 03:53:48 [INFO] Cluster: detected a peer proxysql-1.proxysqlcluster:6032 with mysql_users version 2, epoch 1564718027, diff_check 4. Own version: 1, epoch: 1564714803. Proceeding with remote sync
2019-08-02 03:53:48 [INFO] Cluster: detected peer proxysql-1.proxysqlcluster:6032 with mysql_users version 2, epoch 1564718027
2019-08-02 03:53:48 [INFO] Cluster: Fetching MySQL Users from peer proxysql-1.proxysqlcluster:6032 started
2019-08-02 03:53:48 [INFO] Cluster: Fetching MySQL Users from peer proxysql-1.proxysqlcluster:6032 completed
Tenha em mente que a sincronização automática só acontece se houver uma alteração de configuração no tempo de execução do ProxySQL. Portanto, é vital executar a instrução "LOAD ... TO RUNTIME" antes de poder ver a ação. Não se esqueça de salvar as alterações do ProxySQL no disco para persistência:
mysql> SAVE MYSQL USERS TO DISK;
Limitação
Observe que há uma limitação para essa configuração porque o ProxySQL não suporta salvar/exportar a configuração ativa em um arquivo de configuração de texto que poderíamos usar posteriormente para carregar no ConfigMap para persistência. Há uma solicitação de recurso para isso. Enquanto isso, você pode enviar as modificações para o ConfigMap manualmente. Caso contrário, se os pods forem excluídos acidentalmente, você perderá sua configuração atual porque os novos pods serão inicializados por qualquer definição definida no ConfigMap.
Agradecimentos especiais a Sampath Kamineni, que despertou a ideia desta postagem no blog e forneceu informações sobre os casos de uso e implementação.