Sumário
Prefácio
Este artigo é uma tradução do original disponível aqui e que foi escrito por Denis Simonov e patrocinado pela IBSurgeon. O material está licenciado sob a Public Documentation License https://www.firebirdsql.org/file/documentation/html/en/licenses/pdl/public-documentation-license.html
Desenvolvedores de aplicativos e administradores que usam o SGBD Firebird muitas vezes se perguntam: e se eles implantarem o Firebird na nuvem e o acessarem por uma conexão com a Internet?
No entanto, depois de testar essa configuração, muitos ficam desapontados, pois a velocidade de transferência de dados em redes de alta latência (como a Internet) deixa muito a desejar.
Na maioria dos casos, a velocidade de busca de dados de cursores gerados por consultas SQL é aceitável, mas assim que campos BLOB (dados binários ou de texto) aparecem em tais consultas, a velocidade de transferência de dados cai catastroficamente.
Neste artigo, discutiremos como os BLOBs são transmitidos pela rede, os desafios que os usuários enfrentam ao usar o Firebird em redes de alta latência (trabalhando pela Internet) e exploraremos soluções para esses problemas.
Também abordaremos as melhorias na transmissão de BLOBs nas versões mais recentes do Firebird (5.0.2 e 5.0.3).
1. Aplicação e Banco de Dados para Teste
Para demonstrar várias maneiras de trabalhar com campos BLOB, bem como medições de desempenho, um pequeno aplicativo de teste foi escrito, cujos códigos-fonte estão disponíveis em https://github.com/IBSurgeon/fb-blob-test. Na mesma página, você pode baixar um assembly pronto para Windows x64 e um banco de dados de teste.
Este aplicativo testa o desempenho da transferência apenas de campos BLOB de texto, mas os mesmos mecanismos podem ser aplicados a BLOBs binários.
Para demonstrar a transferência de BLOBs pela rede, precisaremos de um banco de dados contendo tabelas com campos BLOB, e é desejável que o tamanho desses campos BLOB varie de muito pequeno a médio.
Para este propósito, você pode usar os códigos-fonte de algum projeto Open Source, por exemplo, a biblioteca UDR lucene_udr. O conteúdo dos arquivos será armazenado em uma tabela com a seguinte estrutura:
CREATE TABLE BLOB_SAMPLE (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY,
FILE_NAME VARCHAR(255) CHARACTER SET UTF8 NOT NULL,
CONTENT BLOB SUB_TYPE TEXT CHARACTER SET UTF8
);
ALTER TABLE BLOB_SAMPLE ADD PRIMARY KEY (ID);
ALTER TABLE BLOB_SAMPLE ADD UNIQUE (FILE_NAME);
Como o projeto não é grande, o número de arquivos de código-fonte nele não é tão grande quanto gostaríamos. Para tornar os resultados do teste mais visuais em números, aumentaremos o número de registros BLOB para 10.000.
Para fazer isso, criaremos uma tabela separada BLOB_TEST com a seguinte estrutura:
RECREATE TABLE BLOB_TEST (
ID BIGINT GENERATED BY DEFAULT AS IDENTITY,
SHORT_CONTENT VARCHAR(8191) CHARACTER SET UTF8,
CONTENT BLOB SUB_TYPE TEXT CHARACTER SET UTF8,
SHORT_BLOB BOOLEAN DEFAULT FALSE NOT NULL,
CONSTRAINT PK_BLOB_TEST PRIMARY KEY (ID)
);
Aqui removemos o campo de armazenamento de nome de arquivo FILE_NAME, mas adicionamos o campo SHORT_CONTENT. Preencheremos este campo se o conteúdo do campo BLOB CONTENT puder ser armazenado inteiramente em um campo do tipo VARCHAR(8191) CHARACTER SET UTF8. Também adicionaremos o campo SHORT_BLOB, que é uma indicação de que o BLOB é "curto" (cabe em VARCHAR). Precisaremos desses campos ao realizar vários testes comparativos.
Portanto, precisamos preencher a tabela BLOB_TEST a partir da tabela BLOB_SAMPLE, para que a tabela de destino tenha 10.000 registros. Para fazer isso, usaremos o seguinte script:
SET TERM ^;
EXECUTE BLOCK
AS
DECLARE I INTEGER = 0;
DECLARE IS_SHORT BOOLEAN;
BEGIN
WHILE (TRUE) DO
BEGIN
FOR
SELECT
ID,
CONTENT,
CHAR_LENGTH(CONTENT) AS CH_L
FROM BLOB_SAMPLE
ORDER BY FILE_NAME
AS CURSOR C
DO
BEGIN
I = I + 1;
-- O conteúdo do BLOB é colocado em uma variável de string
-- com um comprimento de 8191 caracteres
IS_SHORT = (C.CH_L < 8191);
-- se o BLOB for curto, nós o escrevemos no campo VARCHAR
INSERT INTO BLOB_TEST (
SHORT_CONTENT,
CONTENT,
SHORT_BLOB
)
VALUES (
IIF(:IS_SHORT, C.CONTENT, NULL),
:C.CONTENT,
:IS_SHORT
);
-- sai quando 10000 registros são inseridos
IF (I = 10000) THEN EXIT;
END
END
END^
SET TERM ;^
COMMIT;
2. BLOB vs VARCHAR
Vamos tentar descobrir por que trabalhar em uma rede de alta latência (canal de Internet) se torna desconfortável se as consultas selecionarem dados contendo colunas BLOB.
Para fazer isso, realizaremos um teste comparativo de transferência dos mesmos dados quando esses dados estão localizados nos campos VARCHAR e BLOB. O teste será realizado usando o fbclient versão 5.0.1 (versões anteriores se comportam de forma semelhante).
Lembre-me de que no Firebird uma coluna VARCHAR pode conter 32.765 bytes, se contiver texto na codificação UTF8, então VARCHAR pode conter até 8.191 caracteres (UTF-8 usa codificação de comprimento variável de 1 a 4 bytes por caractere). É por isso que na tabela BLOB_TEST a coluna SHORT_CONTENT é definida como:
SHORT_CONTENT VARCHAR(8191) CHARACTER SET UTF8
Primeiro, vamos ver as estatísticas de execução de uma consulta que transfere dados usando uma coluna BLOB cujo comprimento não excede 8191 caracteres:
SELECT
ID,
CONTENT
FROM BLOB_TEST
WHERE SHORT_BLOB IS TRUE
FETCH FIRST 1000 ROWS ONLY
Estatísticas:
- Tempo decorrido: 36544ms
- ID máximo: 1700
- Contagem de registros: 1000
- Tamanho do conteúdo: 3366000 bytes
Agora vamos compará-lo com as estatísticas da execução da consulta usando uma coluna VARCHAR:
SELECT
ID,
SHORT_CONTENT
FROM BLOB_TEST
WHERE SHORT_BLOB IS TRUE
FETCH FIRST 1000 ROWS ONLY
Estatísticas:
- Tempo decorrido: 574ms
- ID máximo: 1700
- Contagem de registros: 1000
- Tamanho do conteúdo: 3366000 bytes
Uau, a transferência de dados usando uma coluna VARCHAR é 64 vezes mais rápida!
3. BLOB vs VARCHAR + compressão de fio
Bem, vamos tentar habilitar a compressão de fio. Isso pode ser feito especificando o parâmetro WireCompression=True ao conectar-se ao banco de dados.
Teste de transferência de BLOBS curtos:
SELECT
ID,
CONTENT
FROM BLOB_TEST
WHERE SHORT_BLOB IS TRUE
FETCH FIRST 1000 ROWS ONLY
Tempo decorrido: 36396ms
Teste de transferência de dados no tipo VARCHAR(8191):
SELECT
ID,
SHORT_CONTENT
FROM BLOB_TEST
WHERE SHORT_BLOB IS TRUE
FETCH FIRST 1000 ROWS ONLY
Tempo decorrido: 489ms
Teste de transferência de BLOBs curtos e de tamanho médio:
SELECT
ID,
CONTENT
FROM BLOB_TEST
FETCH FIRST 1000 ROWS ONLY
Tempo decorrido: 38107ms
A situação quase não mudou. Vamos tentar entender os motivos.
4. Como os dados BLOB são transmitidos pela rede
Para entender por que isso acontece, precisamos nos aprofundar no funcionamento interno do protocolo de rede do servidor Firebird.
Primeiro e mais importante, é importante entender dois aspectos fundamentais. O protocolo de rede e a API são projetados para lidar com grandes objetos binários ou strings longas (BLOBs):
- em pequenos blocos (não maiores que 64 KB);
- em um modo adiado (preguiçoso).
Aqui está uma explicação simplificada do que acontece. Quando o cursor é aberto, um pacote de rede correspondente op_execute2 é enviado ao servidor. A chamada fetchNext envia um pacote de rede op_fetch para o servidor, após o qual o servidor retorna tantos registros quantos couberem no buffer de rede.
As chamadas subsequentes de fetchNext não enviarão pacotes de rede para o servidor, mas lerão o próximo registro do buffer até que o buffer se esgote. Quando o buffer estiver vazio, a chamada fetchNext enviará novamente um pacote de rede op_fetch para o servidor. Essa abordagem reduz significativamente o número de viagens de ida e volta. Uma viagem de ida e volta refere-se ao envio de um pacote de rede para o servidor e ao recebimento de um pacote de resposta do servidor de volta para o cliente. Quanto menos viagens de ida e volta, maior a eficiência do protocolo de rede.
Para a consulta SQL:
SELECT
ID,
SHORT_CONTENT
FROM BLOB_TEST
WHERE SHORT_BLOB IS TRUE
FETCH FIRST 1000 ROWS ONLY
A mensagem de saída pode ser mapeada para a seguinte estrutura:
struct message {
int64_t id; // valor do campo ID
short idNull; // indicador NULL para o campo ID
struct {
unsigned short length; // comprimento real do campo VARCHAR em bytes
char str[8191*4]; // buffer para dados de strixconteudo